Skip to content

Commit

Permalink
Merge 2d8d619 into ffea284
Browse files Browse the repository at this point in the history
  • Loading branch information
Stéphane Caron committed Jan 17, 2023
2 parents ffea284 + 2d8d619 commit d43fbb0
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 78 deletions.
71 changes: 58 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,62 @@ on:
branches: [ main ]

jobs:
lint:
name: "Code style"
runs-on: ubuntu-latest

steps:
- name: "Checkout sources"
uses: actions/checkout@v3

- name: "Set up Python ${{ matrix.python-version }}"
uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python-version }}"

- name: "Install dependencies"
run: |
python -m pip install --upgrade pip
# tox version: https://github.com/tox-dev/tox/issues/2778
python -m pip install tox==3.28.0
- name: "Test with tox for ${{ matrix.os }}"
run: |
tox -e lint
coverage:
name: "Coverage"
runs-on: ubuntu-latest

steps:
- name: "Checkout sources"
uses: actions/checkout@v3

- name: "Set up Python 3.8"
uses: actions/setup-python@v4
with:
python-version: "3.8"

- name: "Install dependencies"
run: |
python -m pip install --upgrade pip
# tox version: https://github.com/tox-dev/tox/issues/2778
python -m pip install coveralls tox==3.28.0
- name: "Check code coverage"
run: |
tox -e coverage
- name: "Coveralls"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
coveralls --service=github
test:
name: "${{ matrix.os }}, python-${{ matrix.python-version }}"
name: "Test ${{ matrix.os }} with python-${{ matrix.python-version }}"
runs-on: ${{ matrix.os }}

env:
USING_COVERAGE: "3.8"

strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
Expand All @@ -23,23 +72,19 @@ jobs:
- name: "Checkout sources"
uses: actions/checkout@v3

- name: "Set up Python"
- name: "Set up Python ${{ matrix.python-version }}"
uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python-version }}"

- name: "Install dependencies"
run: |
python -m pip install --upgrade pip
python -m pip install coveralls tox tox-gh-actions
# tox version: https://github.com/tox-dev/tox/issues/2778
python -m pip install tox==3.28.0 tox-gh-actions
- name: "Run tox targets for ${{ matrix.python-version }}"
- name: "Test with tox for ${{ matrix.os }}"
run: |
tox
- name: "Coveralls"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
coveralls --service=github
if: ${{ matrix.os == 'ubuntu-latest' && contains(env.USING_COVERAGE, matrix.python-version) }}
PLATFORM: ${{ matrix.os }}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Added

- RateLimiter: ``dt`` property
- RateLimiter: ``next_tick`` property

### Changed

- Attributes are now read-only
- RateLimiter: ``period`` becomes read-only
- RateLimiter: ``slack`` becomes read-only

## [0.2.0] - 2022/12/5

### Added
Expand Down
6 changes: 2 additions & 4 deletions loop_rate_limiters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Loop rate limiters.
"""
"""Loop rate limiters."""

__version__ = "0.2.0"
__version__ = "0.2.1rc0"

from .async_rate_limiter import AsyncRateLimiter
from .rate_limiter import RateLimiter
Expand Down
16 changes: 5 additions & 11 deletions loop_rate_limiters/async_rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module provides a non-blocking loop frequency limiter for asyncio.
"""Non-blocking loop frequency limiter for asyncio.
Note that there is a difference between a (non-blocking) rate limiter and a
(blocking) synchronous clock, which lies in the behavior when skipping cycles.
Expand All @@ -31,9 +30,7 @@


class AsyncRateLimiter:

"""
Loop frequency regulator.
"""Loop frequency regulator.
Calls to :func:`sleep` are non-blocking most of the time but become
blocking close to the next clock tick to get more reliable loop
Expand Down Expand Up @@ -67,8 +64,7 @@ class AsyncRateLimiter:
slack: float

def __init__(self, frequency: float, name: str = "rate_limiter"):
"""
Initialize rate limiter.
"""Initialize rate limiter.
Args:
frequency: Desired loop frequency in hertz.
Expand All @@ -86,17 +82,15 @@ def __init__(self, frequency: float, name: str = "rate_limiter"):
self.slack = 0.0

async def remaining(self) -> float:
"""
Get the time remaining until the next expected clock tick.
"""Get the time remaining until the next expected clock tick.
Returns:
Time remaining, in seconds, until the next expected clock tick.
"""
return self._next_tick - self._loop.time()

async def sleep(self, block_duration: float = 5e-4):
"""
Sleep the duration required to regulate the loop frequency.
"""Sleep the duration required to regulate the loop frequency.
This function is meant to be called once per loop cycle.
Expand Down
75 changes: 43 additions & 32 deletions loop_rate_limiters/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
#
# Copyright 2022 Stéphane Caron
# Copyright 2023 Inria
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -15,61 +16,71 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Basic rate limiter.
"""
"""Basic rate limiter."""

from time import perf_counter, sleep


class RateLimiter:
"""Regulate the frequency between calls to the same instruction.
"""
Regulate the frequency between calls to the same instruction in e.g. a loop
or callback.
This rate limiter is in essence the same as rospy.Rate_. It assumes
Python's performance counter never jumps backward nor forward, so that it
does not handle such cases contrary to rospy.Rate_.
This rate limniter is meant to be used in e.g. a loop or callback function.
It is, in essence, the same as rospy.Rate_. It assumes Python's performance
counter never jumps backward nor forward, so that it does not handle such
cases contrary to rospy.Rate_.
.. _rospy.Rate:
https://github.com/ros/ros_comm/blob/noetic-devel/clients/rospy/src/rospy/timer.py
Attributes:
period: Desired period between two calls to :func:`sleep`, in seconds.
slack: Duration in seconds remaining until the next tick at the
end of the last call to :func:`sleep`.
"""

_next_tick: float
period: float
slack: float
__period: float
__slack: float
__next_tick: float

def __init__(self, frequency: float):
"""
Initialize rate limiter.
"""Initialize rate limiter.
Args:
frequency: Desired frequency in hertz.
"""
period = 1.0 / frequency
self._next_tick = perf_counter() + period
self.period = period
self.__next_tick = perf_counter() + period
self.__period = period

def remaining(self) -> float:
@property
def dt(self) -> float:
"""Desired period between two calls to :func:`sleep`, in seconds."""
return self.__period

@property
def next_tick(self) -> float:
"""Time of next clock tick."""
return self.__next_tick

@property
def period(self) -> float:
"""Desired period between two calls to :func:`sleep`, in seconds."""
return self.__period

@property
def slack(self) -> float:
"""Slack duration computed at the last call to :func:`sleep`.
This duration is in seconds.
"""
Get the time remaining until the next expected clock tick.
return self.__slack

def remaining(self) -> float:
"""Get the time remaining until the next expected clock tick.
Returns:
Time remaining, in seconds, until the next expected clock tick.
"""
return self._next_tick - perf_counter()
return self.__next_tick - perf_counter()

def sleep(self):
"""
Sleep the duration required to regulate the frequency between calls.
"""
self.slack = self._next_tick - perf_counter()
if self.slack > 0.0:
sleep(self.slack)
self._next_tick = perf_counter() + self.period
"""Sleep for the duration required to regulate inter-call frequency."""
self.__slack = self.__next_tick - perf_counter()
if self.__slack > 0.0:
sleep(self.__slack)
self.__next_tick = perf_counter() + self.__period
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,20 @@ line-length = 79

[tool.flit.module]
name = "loop_rate_limiters"

[tool.ruff]
line-length = 79
select = [
# pyflakes
"F",
# pycodestyle
"E",
"W",
# isort
"I001",
# pydocstyle
"D"
]

[tool.ruff.pydocstyle]
convention = "google"
12 changes: 6 additions & 6 deletions tests/test_rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@

class TestRate(unittest.TestCase):
def setUp(self):
"""
Initialize a rate with 1 ms period.
"""
"""Initialize a rate with 1 ms period."""
self.rate = RateLimiter(frequency=1000.0)

def test_init(self):
"""
Constructor completed.
"""
"""Constructor completed."""
self.assertIsNotNone(self.rate)

def test_period_dt(self):
"""Check that period and dt are the same."""
self.assertAlmostEqual(self.rate.period, self.rate.dt)

def test_remaining(self):
"""
After one period has expired, the "remaining" time becomes negative.
Expand Down
28 changes: 16 additions & 12 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ python =
3.10: py310

[testenv]
commands =
python -m unittest discover

[testenv:coverage]
deps =
black
coverage
flake8
mccabe
mypy
pylint
coverage >=5.5
commands =
black loop_rate_limiters
flake8 loop_rate_limiters
pylint loop_rate_limiters --exit-zero --rcfile=tox.ini
mypy loop_rate_limiters --ignore-missing-imports
coverage erase
coverage run -m unittest discover
coverage report --include="loop_rate_limiters/*"

[flake8]
max-line-length = 88
[testenv:lint]
deps =
black >=22.10.0
mypy >=0.812
pylint >=2.8.2
ruff >=0.0.220
commands =
black loop_rate_limiters
ruff loop_rate_limiters
pylint loop_rate_limiters --exit-zero --rcfile=tox.ini
mypy loop_rate_limiters --ignore-missing-imports

0 comments on commit d43fbb0

Please sign in to comment.