Package: oclock
Aboutoclockis a Python 3 package. Its main goal is to provide a simple way to create timed loops with constant time intervals and no drift. It also provides various other timing tools and a GUI time.Timed loopsNo drift, timed loops are based on theTimerclass,either using thecheckpt()method at a location in a for/while loop acting as a reference point to maintain constant duration from one loop to the next,or using the@loopor@interactiveloopdecorators that useTimerandcheckpt()in the background.The timing (interval) and execution (pause/stop etc.) can be modified in real time thanks to cancellable sleeping times.Other toolsEvent: class mimickingthreading.Event()but with much better sleeping time accuracy.Countdown: a class that starts a GUI countdown timer.parse_time()function: returns adatetime.timedeltafrom a time string (e.g.':2:25').measure_time()andmeasure_duration()functions: are context managers for measuring time and execution times / time uncertainty of encapsulated commands.after()allows the user to run a function after a pre-defined waiting time.Note that theTimerclass can also be used as a regular chronometer with its methodspause(),resume(),stop()etc.Quick startInstallpipinstalloclockTimed LoopsTheTimerclass is mostly designed to create loops of constant duration without drift, while allowing immediate modification/cancellation (sleep time interruption) in threaded environments. It can also be used as a regular chronometer.Below are some peudo-code quick-start examples. For complete, working examples, see:Examples.ipynbnotebook (https://github.com/ovinc/oclock/blob/master/Example.ipynb)example.pyscript (https://github.com/ovinc/oclock/blob/master/example.py)Constant-duration loopsThe most basic use of theTimer()class in Python code to create a loop of constant duration is:fromoclockimportTimertimer=Timer(interval=2)# Loops will be of total duration 2 secondswhilecondition:my_function()# can be of any duration between 0 and 2 secondstimer.checkpt()Thecheckpt()method waits the adequate amount of time to make the loop of constant duration, without drift (using a target regularly spaced in time);conditioncan include timer methods and attributes, e.g.timer.elapsed_time < max_time.Note that ifmy_function()takes longer to execute than the required time interval, the Timer class does not try to compensate the extra time by making the next loop shorter. It just aims at making the total duration of the next loop be the requested interval again (seeBehavior when interval is exceededsection below).The same behavior can be achieved using the@loopdecorator:fromoclockimportlooptimer=Timer(interval=2)# Loops will be of total duration 2 seconds@loop(timer)defmy_function():...Then, callingmy_function()will execute the contents of the function in a repeated manner.The@loopexecution exits automatically if the timer is stopped.
Thus, It is useful to include a condition inmy_functionto exit the loop when needed, e.g.iftimer.elapsed_time>t_max:timer.stop()Interactive modification/cancellationThe timer is also modifiable (change time interval) and cancellable in real time (i.e. even when the timer is in acheckpt()waiting phase). To do so, it must be accessed by another thread that runs concurrently. For example:fromoclockimportTimerfromthreadingimportThreaddefuser_input(timer):"""Threaded command line input to change time interval or exit timer."""whilenottimer.is_stopped:a=input()try:dt=float(a)exceptValueError:# immediately cancel timer & exit all loops/threadstimer.stop()else:# immediately change interval to input valuetimer.interval=dttimer=Timer(interval=2)Thread(target=user_input,args=(exit_event,timer)).start()whilenottimer.is_stopped:my_function()timer.checkpt()During operation, theTimerobject can be paused, resumed, stopped and reset using the correspondingTimermethods (seeRegular Timerparagraph below). Theoclockmodule also provides a simple command line interface to create a timed loop for a function and interact with it dynamically using the@interactiveloopdecorator:fromoclockimportinteractiveloop@interactiveloop(interval=2)defmy_function():...Now whenmy_function()is called, an interactive CLI thread starts at the same time where the user can pause/resume/reset/stop the timer in real time, change its interval, and print timing information.Regular TimerAlthough not its main purpose, theTimerclass can be used as a regular chronometer with the following methods (no need to be in a threaded environment, although the methods below whould work and be cancellable in a threaded environment):fromoclockimportTimer# The timer starts counting time immediately upon instantiation.timer=Timer()# Temporarily pause, then resume timertimer.pause()timer.resume()# Stop and restart timer completelytimer.stop()timer.reset()# note: can be called without calling stop() first# At any time, the elapsed time and total pause duration can be accessed withtimer.elapsed_timetimer.pause_timetimer.total_time# sum of the two other timesImportant Note: Do not use thecheckpt()method after apause()call if not in a threaded environment, this will cause the program to be in a perpetual waiting state. In a threaded environment, callresume()to unpause.DetailsSeeTimer Class detailssection below for all methods, properties and attributes and theDevelopmentsection below for accuracy information.Event classTheoclock.Eventclass mimicksthreading.Event(https://docs.python.org/3/library/threading.html#event-objects) but provides much better sleep time accuracy.Available methods are the same as forthreading.Event:set()clear()is_set()wait()Below are comments from Chris D. who originally posted the code for this class on StackOverflow (seeContributorsat the end of this file):Internally, it uses a combination of a time.sleep() loop and a busy loop for greatly increased precision. The sleep loop runs in a separate thread so that the blocking wait() call in the main thread can still be immediately interrupted. When the set() method is called, the sleep thread should terminate shortly afterwards. Also, in order to minimize CPU utilization, I made sure that the busy loop will never run for more than 3 milliseconds.Countdown GUIA simple graphical countdown timer based on theTimerclass. It can be used either as a python main program from a shell, or as a function in Python code or console.From a terminal:python-moclock1:45:00# start timer of duration 1 hour 45 minutespython-moclock1:45:# exactly the same as abovepython-moclock00:02:00# start 2-minute timerpython-moclock:2:# exactly the same as abovepython-moclock00:00:05# start 5-second timerpython-moclock::5# exactly the same as aboveIn python:fromoclockimportCountdownCountdown('1:45:')# start timer of duration 1 hour 45 minutes(the inputs are of the same format as from the terminal, see above).When countdown is finished, 'Done' is displayed for 5 seconds in the GUI while the console displaysCountdown finishedand emits a sound. Then the time passed since the end of countdown is displayed as a negative value in red. The program stops when the GUI window is closed.Parse time functionTheparse_time()function is used in the argument parsing of the countdown GUI from a terminal (see above). It transforms a string in the form'h:m:s'into adatetime.timedeltaobject. Inputs of the form e.g.'::5'or:2:,'3:30:'are acceptable for 5 seconds, 2 minutes, and 3.5 hours, respectively.>>>parse_time(':2:30')datetime.timedelta(seconds=150)Context managers to record timing of commandsThemeasure_time()function is a context manager that saves the timing info of the encapsulated commands. This can be e.g. used in scientific experiments to get the time and time uncertainty associated with a measurement.fromoclockimportmeasure_time,measure_duration# ----------------------------------------------------------------------------# Examples where one just wants to get timing info ---------------------------# ----------------------------------------------------------------------------withmeasure_time()astiming:my_function()print(timing)# Out: {'time (unix)': 1604780958.0705943, 'dt (s)': 0.6218999624252319}# (dt (s) is half the total duration. To get the total duration instead, do:)withmeasure_duration()asduration:my_function()print(duration)# Out: {'duration (s)': 1.1689763555421325}# ----------------------------------------------------------------------------# Example where the timing info is directly added to a data dictionary -------# ----------------------------------------------------------------------------withmeasure_time()asdata:measurement=my_function()# returns e.g. 3.618data['measurement']=measurementprint(data)# Out: {'measurement': 3.618,#       'time (unix)': 1604780958.0705943,#       'dt (s)': 0.6218999624252319}Execute function after waiting timefromoclockimportafterdefmy_function():print('Hello')return3.14after('::2',my_function)# Note: there are options to pass args/kwargs to function#       and also to not block console during waiting.#       (see docstring of after() function)Timer Class detailsInstantiationfromoclockimportTimertimer=Timer(interval=1,name='Timer',warnings=False,precise=False)Parameters:interval(float): timer interval in secondsname(str): optional name for description purposes (repr and warnings)warnings(bool): If True, prints warning when time interval exceededprecise(bool) if True, increase time precision (useful for Windows)Note:Theprecise=Trueoption uses a customEventclass to replacethreading.Event, originally written by Chris D. (see below).Methodstimer.checkpt()# Reference point for constant-duration loops, see abovetimer.pause()# Immediately pause timer and put checkpt() in waiting phasetimer.resume()# Restart the elapsed time counter and unlock checkpt()timer.stop()# Stop counting time and exit checkpt()timer.reset()# Stop and restart timer immediately# Change timer intervaltimer.set_interval(...)# immediately, equivalent to timer.interval = ...timer.set_interval(...,immediate=False)# wait next checkptProperties (settable)timer.interval# get interval (in s)timer.interval+=1# increase interval by 1 secondtimer.interval=10# set interval to 10 seconds.timer.warnings# get current status of warningstimer.warnings=True# activate warnings if time between checkpts too shorttimer.name# optional name (for repr and warnings)timer.name='Countdown timer'# (can also be set during instantiation)Attributes (read-only)# Most useful attributestimer.elapsed_time# Time in seconds since init or last resettimer.pause_time# total time (in s) the timer has been paused.timer.total_time# Sum of the last twoBackground attributes and methods(mostly for development)timer.now()# Reference time used by all methodstimer.start_time# Ref. time corresponding to start/reset of timertimer.next_checkpt_release# Ref. time at which next checkpt waittime is overtimer.interval_exceeded# (bool) True if loop contents take longer to execute than requested intervalNotesAs mentioned previously, methods (and interval setting) take effect immediately, even if the timer is in a waiting phase. It is however possible to wait for the next checkpt to apply a new timer interval, by using theimmediate=Falseoption inset_interval()(see example in theExamples.ipynbnotebook).After callingpause(), thecheckpt()command blocks untilresume()is called, however in the current version afterstop()thecheckpt()becomes non-blocking (equivalent to apass), so that all following lines will be executed immediately and without any waiting time (i.e. as fast as possible if within a loop), untiltimer.reset()is called again. This means that it is useful to pin the condition of the loop to the stopping of the timer (see examples).Timer accuracySeeperformance.pyfile of the module for functions to test the behavior and accuracy of the timer. In particular:fromoclock.performanceimportperformance_testperformance_test(dt=0.01,nloops=1000,fmax=0.99,plot=True,warnings=False,precise=True)tests the timing on 1000 loops of requested duration 0.01 second (10ms), using within the loop a function sleeping for a random amount of time between 0 and 0.99 dt (withplot=Trueoption to see the results on amatplotlibgraph, andwarnings=Falseto not have a printed warning when the execution time of the nested commands exceed the target duration of the loop);precise=Trueuses the timer in precise mode.TheAccuracyTests.mdfile gathers some accuracy results in Unix and Windows environments. In summary:withUnix, time fluctuations are < 0.5 ms with the regular timer, and on the order of 0.01 ms (standard deviation) with the precise timerwithWindows, the regular timer fails quickly as frame rate is increased, due to fluctuations in the ~ 10 ms range. However the precise timer performs even better than in Unix, with fluctuations of less than 0.01 ms (standard deviation).Behavior when interval is exceededAs explained above, it the contents of the loop take longer to execute than the requested time interval, the Timer simply moves on to the next loop but does not try to compensate for the extra time by making the next loop shorter:DevelopmentInstall the package by cloning the GitHub repo (https://github.com/ovinc/oclock.git) and install in editable mode from the root of the repo:pip install -e .TestingPackage requirements to run the tests:pytestnumpyGeneral testing is done withpytest(from the root of the repository):pytest(Note: close the interactive countdown window at the end of the pytest run to finish the test.)Additional testing of interactive command line for real-time timer control can be done by running the example file from the root of the repository.python-mexampleSee alsoAccuracy Testparagraph above to run performance tests for constant-duration loops with theTimerclass.ContributingIssues and Pull requests must be submitted on GitHub (https://github.com/ovinc/oclock) with commits (preferably squashed into a single commit) in branchauthors.Version number is automatically extracted from git tag usingsetuptools_scm. Git tags are added by the repo's maintainer.RequirementsPython 3.x(Tests only made from python 3.5 to python 3.9 included)AuthorOlivier Vincent(ovinc.py@gmail.com)ContributorsTheoclock.Eventclass was originally written by Chris D.(https://stackoverflow.com/questions/48984512/making-a-timer-timeout-inaccuracy-of-threading-event-wait-python-3-6)LicenseGNU GPLv3, seeLICENSEfile