Skip to content

Commit

Permalink
Allow to specify a custom time function (#68)
Browse files Browse the repository at this point in the history
Expose PID.time_fn to allow specifying a custom time function.
  • Loading branch information
m-lundberg committed Oct 2, 2022
1 parent e518b0c commit 505a18f
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ build/
.tox/
*.pyc
.idea/
.vscode/
.envrc
build.sh
total_downloads.py
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Ability to override the time function by setting PID.time_fn to whichever function to use.

### Changed

- Rename the module `PID` to `pid` to avoid the shadowing from the `PID` class
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ def pi_clip(angle):
pid.error_map = pi_clip
```

#### Overriding time function
By default, the PID uses `time.monotonic()` (or if not available, `time.time()` as fallback) to get the current time on each invocation. The time function can be overridden by setting `PID.time_fn` to whichever function you want to use. For example, to use the [MicroPython time.ticks_us()](https://docs.micropython.org/en/latest/library/time.html#time.ticks_us):
```python
import time
pid.time_fn = time.ticks_us
```

## Tests
Use the following to run tests:
```
Expand Down
19 changes: 9 additions & 10 deletions simple_pid/pid.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ def _clamp(value, limits):
return value


try:
# Get monotonic time to ensure that time deltas are always positive
_current_time = time.monotonic
except AttributeError:
# time.monotonic() not available (using python < 3.3), fallback to time.time()
_current_time = time.time


class PID(object):
"""A simple PID controller."""

Expand Down Expand Up @@ -80,6 +72,13 @@ def __init__(
self._last_error = None
self._last_input = None

try:
# Get monotonic time to ensure that time deltas are always positive
self.time_fn = time.monotonic
except AttributeError:
# time.monotonic() not available (using python < 3.3), fallback to time.time()
self.time_fn = time.time

self.output_limits = output_limits
self.reset()

Expand All @@ -97,7 +96,7 @@ def __call__(self, input_, dt=None):
if not self.auto_mode:
return self._last_output

now = _current_time()
now = self.time_fn()
if dt is None:
dt = now - self._last_time if (now - self._last_time) else 1e-16
elif dt <= 0:
Expand Down Expand Up @@ -248,6 +247,6 @@ def reset(self):

self._integral = _clamp(self._integral, self.output_limits)

self._last_time = _current_time()
self._last_time = self.time_fn()
self._last_output = None
self._last_input = None
3 changes: 1 addition & 2 deletions simple_pid/pid.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ _Tunings = Tuple[float, float, float]

def _clamp(value: Optional[float], limits: _Limits) -> Optional[float]: ...

_current_time: Callable[[], float]

class PID(object):
Kp: float
Ki: float
Expand All @@ -17,6 +15,7 @@ class PID(object):
proportional_on_measurement: bool
differential_on_measurement: bool
error_map: Optional[Callable[[float], float]]
time_fn: Callable[[], float]
def __init__(
self,
Kp: float = ...,
Expand Down
26 changes: 19 additions & 7 deletions tests/test_pid.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,27 @@ def test_sample_time():
assert pid(100) == control


def test_monotonic():
from simple_pid.pid import _current_time
def test_time_fn():
pid = PID()

# Default time function should be time.monotonic, or time.time in older versions of Python
if sys.version_info < (3, 3):
assert _current_time == time.time
assert pid.time_fn == time.time
else:
assert _current_time == time.monotonic
assert pid.time_fn == time.monotonic

i = 0

def time_function():
nonlocal i
i += 1
return i
pid.time_fn = time_function

for j in range(1, 5):
# Call pid a few times and verify that the time function above was used
pid(0)
assert pid._last_time == j


def test_auto_mode():
Expand All @@ -134,12 +148,10 @@ def test_auto_mode():
assert pid(8) == 2

# Last update time should be reset to avoid huge dt
from simple_pid.pid import _current_time

pid.auto_mode = False
time.sleep(1)
pid.auto_mode = True
assert _current_time() - pid._last_time < 0.01
assert pid.time_fn() - pid._last_time < 0.01

# Check that setting last_output works
pid.auto_mode = False
Expand Down

0 comments on commit 505a18f

Please sign in to comment.