Skip to content

Commit

Permalink
Merge f6eaf56 into 03320bb
Browse files Browse the repository at this point in the history
  • Loading branch information
Stéphane Caron committed Sep 1, 2023
2 parents 03320bb + f6eaf56 commit 59bfb3a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 49 deletions.
47 changes: 27 additions & 20 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,57 @@ on:
branches: [ main ]

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

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

- name: "Set up Python ${{ matrix.python-version }}"
- name: "Set up Python 3.8"
uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python-version }}"
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 tox==3.28.0
python -m pip install coveralls tox==3.28.0
- name: "Test with tox for ${{ matrix.os }}"
- name: "Check code coverage"
run: |
tox -e lint
tox -e coverage
coverage:
name: "Coverage"
- name: "Coveralls"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
coveralls --service=github
lint:
name: "Code style"
runs-on: ubuntu-latest

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

- name: "Set up Python 3.8"
- name: "Set up Python ${{ matrix.python-version }}"
uses: actions/setup-python@v4
with:
python-version: "3.8"
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 coveralls tox==3.28.0
- name: "Check code coverage"
run: |
tox -e coverage
python -m pip install tox==3.28.0
- name: "Coveralls"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Test with tox for ${{ matrix.os }}"
run: |
coveralls --service=github
tox -e lint
test:
name: "Test ${{ matrix.os }} with python-${{ matrix.python-version }}"
Expand Down Expand Up @@ -88,3 +88,10 @@ jobs:
tox
env:
PLATFORM: ${{ matrix.os }}

ci_success:
name: "CI success"
runs-on: ubuntu-latest
needs: [coverage, lint, test]
steps:
- run: echo "CI workflow completed successfully"
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

- AsyncRateLimiter: `dt` property

### Changed

- AsyncRateLimiter: `measured_period` is now a property
- AsyncRateLimiter: `next_tick` is now a property
- AsyncRateLimiter: `period` is now a property
- AsyncRateLimiter: `slack` is now a property

## [0.5.0] - 2023/07/25

### Added
Expand Down
86 changes: 57 additions & 29 deletions loop_rate_limiters/async_rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,18 @@ class AsyncRateLimiter:
https://github.com/ros/ros_comm/blob/noetic-devel/clients/rospy/src/rospy/timer.py
Attributes:
measured_period: Actual period in seconds measured at the end of the
last call to :func:`sleep`.
name: Human-readable name used for logging.
period: Desired loop period in seconds.
slack: Duration in seconds remaining until the next tick at the
beginning of the last call to :func:`sleep`.
warn: If set (default), warn when the time between two calls
exceeded the rate clock.
"""

_last_loop_time: float
_loop: asyncio.AbstractEventLoop
_next_tick: float
measured_period: float
__last_loop_time: float
__loop: asyncio.AbstractEventLoop
__measured_period: float
__next_tick: float
__period: float
__slack: float
name: str
period: float
slack: float
warn: bool

def __init__(
Expand All @@ -80,22 +75,53 @@ def __init__(
loop = asyncio.get_event_loop()
period = 1.0 / frequency
assert loop.is_running()
self._last_loop_time = loop.time()
self._loop = loop
self._next_tick = loop.time() + period
self.measured_period = 0.0
self.__last_loop_time = loop.time()
self.__loop = loop
self.__measured_period = 0.0
self.__next_tick = loop.time() + period
self.__period = period
self.__slack = 0.0
self.name = name
self.period = period
self.slack = 0.0
self.warn = warn

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

@property
def measured_period(self) -> float:
"""Period measured at the end of the last call to :func:`sleep`.
This duration is in seconds.
"""
return self.__measured_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.
"""
return self.__slack

async 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 - self._loop.time()
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.
Expand All @@ -116,17 +142,19 @@ async def sleep(self, block_duration: float = 5e-4):
average error with a single asyncio.sleep). Empirically a block
duration of 0.5 ms gives good behavior at 400 Hz or lower.
"""
self.slack = self._next_tick - self._loop.time()
if self.slack > 0.0:
block_time = self._next_tick - block_duration
while self._loop.time() < self._next_tick:
if self._loop.time() < block_time:
self.__slack = self.__next_tick - self.__loop.time()
if self.__slack > 0.0:
block_time = self.__next_tick - block_duration
while self.__loop.time() < self.__next_tick:
if self.__loop.time() < block_time:
await asyncio.sleep(1e-5) # non-zero sleep duration
elif self.slack < -0.1 * self.period and self.warn:
elif self.__slack < -0.1 * self.__period and self.warn:
logging.warning(
"%s is late by %f [ms]", self.name, round(1e3 * self.slack, 1)
"%s is late by %f [ms]",
self.name,
round(1e3 * self.__slack, 1),
)
loop_time = self._loop.time()
self.measured_period = loop_time - self._last_loop_time
self._last_loop_time = loop_time
self._next_tick = loop_time + self.period
loop_time = self.__loop.time()
self.__measured_period = loop_time - self.__last_loop_time
self.__last_loop_time = loop_time
self.__next_tick = loop_time + self.__period
4 changes: 4 additions & 0 deletions tests/test_async_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ async def test_init(self):
"""
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)

async def test_remaining(self):
"""
After one period has expired, the "remaining" time becomes negative.
Expand Down

0 comments on commit 59bfb3a

Please sign in to comment.