├── LICENSE ├── README.md ├── pacemaker └── pacemaker.py └── pyproject.toml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Brandon Rohrer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pacemaker 2 | For controlling time per iteration loop in Python. 3 | 4 | A carefully regulated loop comes in handy for managing rate-limited APIs, 5 | frame refreshes in animations, and cycles in real-time simulations. 6 | This package provides an accurate metronome for your code to keep time with, 7 | good up to 1 MHz (one microsecond per iteration). 8 | 9 | The pacemaker package is intentionally minimalistic and has no external dependencies. 10 | It's a glorified snippet. The whole package's code is shorter than this README. 11 | Works on Linux, Windows, and MacOS. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | pip install pacemaker-lite 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```python 22 | from pacemaker.pacemaker import Pacemaker 23 | 24 | iterations_per_second = 2.5 25 | pm = Pacemaker(iterations_per_second) 26 | 27 | for i in range(100): 28 | _ = pm.beat() 29 | print(i) 30 | ``` 31 | 32 | The repetitive import line can be better understood as 33 | ```python 34 | from pacemaker_package.pacemaker_module import PacemakerClass 35 | ``` 36 | 37 | ## What it does 38 | 39 | The Pacemaker class keeps a loop operating at a steady rhythm, 40 | according to a wall clock. It's useful for coordinating the activities 41 | of several asynchronous and real-time processes. 42 | 43 | Initialize Pacemaker(clock_frequency) with the desired number of iterations per second. 44 | Call Pacemaker.beat() in a loop to keep it cycling 45 | at the clock frequency. 46 | 47 | If the pacemaker experiences a delay, it will allow faster iterations to try 48 | to catch up. Heads up: because of this, any individual iteration might end up being much 49 | shorter than suggested by the pacemaker's target rate. 50 | 51 | beat() returns the amount of time that it was off from the ideal pace, counting 52 | back from the first beat. 53 | ```python 54 | off_by = pm.beat() 55 | ``` 56 | A return value of 0 means it 57 | was exactly correct. If it's higher than that, it means that the 58 | cycle came in late. 59 | It will usually be off by a little. You can create a 60 | set of checks on the return value if it's important to keep the cycle 61 | time tightly controlled. 62 | ```python 63 | a_little_late = .1 # seconds 64 | a_lot_late = .5 # seconds 65 | off_by = pm.beat() 66 | if off_by > a_lot_late: 67 | raise RuntimeError(f"Last beat was running behind by {elapsed} seconds.") 68 | if off_by > a_little_late: 69 | print(f"Last beat was running behind by {elapsed} seconds.") # or log this 70 | ``` 71 | 72 | For a deep dive on what the pacemaker does and why, check out 73 | [Chapter 2 of How to Train Your Robot: Keeping Time with Python](https://brandonrohrer.com/httyr2pdf). 74 | -------------------------------------------------------------------------------- /pacemaker/pacemaker.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Pacemaker: 5 | """ 6 | The Pacemaker class keeps a loop operating at a steady rhythm, 7 | according to a wall clock. It's useful for coordinating the activities 8 | of several asynchronous and real-time processes. 9 | 10 | Initialize Pacemaker(clock_freq) with the desired clock frequency in Hz. 11 | Call Pacemaker.beat() in a loop to keep it cycling 12 | at the clock frequency. 13 | 14 | beat() returns the amount of time elapsed for that cycle that was 15 | in excess of the desired clock period. A return value of 0 means it 16 | was exactly correct. If it's higher than that, it means that the 17 | cycle took a little longer to run than desired. 18 | It will usually be off by a little. You can create a 19 | set of checks on the return value if it's important to keep the cycle 20 | time tightly controlled. 21 | 22 | For consistency, all the time units are in seconds. 23 | 24 | See brandonrohrer.com/httyr2 for a detailed development of the method. 25 | """ 26 | 27 | def __init__(self, clock_freq_Hz): 28 | self.clock_period = 1 / float(clock_freq_Hz) 29 | self.last_run_completed = time.monotonic() 30 | self.start_time = time.monotonic() 31 | self.i_iter = -1 32 | 33 | def beat(self): 34 | self.i_iter += 1 35 | end = self.start_time + (self.i_iter + 1) * self.clock_period 36 | 37 | sleep_time = end - time.monotonic() 38 | if sleep_time > 0: 39 | time.sleep(sleep_time) 40 | 41 | this_run_completed = time.monotonic() 42 | dt = this_run_completed - self.last_run_completed 43 | overtime = dt - self.clock_period 44 | self.last_run_completed = this_run_completed 45 | return overtime 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pacemaker-lite" 3 | version = "0.1.1" 4 | authors = [ 5 | { name="Brandon Rohrer", email="brohrer@gmail.com" }, 6 | ] 7 | description = "For controlling the duration of iteration loops" 8 | readme = "README.md" 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Programming Language :: Python :: 3", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | ] 15 | 16 | [build-system] 17 | requires = ["setuptools>=61.0"] 18 | build-backend = "setuptools.build_meta" 19 | 20 | [project.urls] 21 | Repository = "https://github.com/brohrer/pacemaker" 22 | Documentation = "https://brandonrohrer.com/httyr2pdf" 23 | --------------------------------------------------------------------------------