-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3f89198
Showing
3 changed files
with
142 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
venv/ | ||
docs/build | ||
__pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import time | ||
|
||
|
||
def _clamp(value, limits): | ||
lower, upper = limits | ||
if value is None: | ||
return None | ||
elif upper is not None and value > upper: | ||
return upper | ||
elif lower is not None and value < lower: | ||
return lower | ||
return value | ||
|
||
|
||
class PID: | ||
""" | ||
A simple PID controller. No fuss. | ||
""" | ||
def __init__(self, Kp=1.0, Ki=0.0, Kd=0.0, setpoint=0, sample_time=0.01, output_limits=(None, None), auto_mode=True, proportional_on_measurement=False): | ||
""" | ||
:param Kp: The value for the proportional gain Kp | ||
:param Ki: The value for the integral gain Ki | ||
:param Kd: The value for the derivative gain Kd | ||
:param setpoint: The initial setpoint that the PID will try to achieve | ||
:param sample_time: The time in seconds which the controller should wait before generating a new output value. The PID works best when it is constantly called (eg. during a loop), but with a sample time set so that the time difference between each update is (close to) constant. If set to None, the PID will compute a new output value every time it is called. | ||
:param output_limits: The initial output limits to use, given as an iterable with 2 elements, for example: (lower, upper). The output will never go below the lower limit or above the upper limit. Either of the limits can also be set to None to have no limit in that direction. Setting output limits also avoids integral windup, since the integral term will never be allowed to grow outside of the limits. | ||
:param auto_mode: Whether the controller should be enabled (in auto mode) or not (in manual mode) | ||
:param proportional_on_measurement: Whether the proportional term should be calculated on the input directly rather than on the error (which is the traditional way). Using proportional-on-measurement avoids overshoot for some types of systems. | ||
""" | ||
self.Kp, self.Ki, self.Kd = Kp, Ki, Kd | ||
self.setpoint = setpoint | ||
self.sample_time = sample_time | ||
|
||
self._min_output, self._max_output = output_limits | ||
self._auto_mode = auto_mode | ||
self.proportional_on_measurement = proportional_on_measurement | ||
|
||
self._error_sum = 0 | ||
|
||
self._last_time = time.time() | ||
self._last_output = None | ||
self._proportional = 0 | ||
self._last_input = None | ||
|
||
def __call__(self, input_): | ||
""" | ||
Call the PID controller with *input_* and calculate and return a control output if sample_time seconds has passed | ||
since the last update. If no new output is calculated, return the previous output instead (or None if no value | ||
has been calculated yet). | ||
""" | ||
if not self.auto_mode: | ||
return self._last_output | ||
|
||
dt = time.time() - self._last_time | ||
|
||
if self.sample_time is not None \ | ||
and dt < self.sample_time \ | ||
and self._last_output is not None: | ||
# only update every sample_time seconds | ||
return self._last_output | ||
|
||
# compute error terms | ||
error = self.setpoint - input_ | ||
self._error_sum += self.Ki * error * dt | ||
d_input = input_ - (self._last_input if self._last_input is not None else input_) | ||
|
||
# compute the proportional term | ||
if not self.proportional_on_measurement: | ||
# regular proportional-on-error, simply set the proportional term | ||
self._proportional = self.Kp * error | ||
else: | ||
# add the proportional error on measurement to error_sum | ||
self._error_sum -= self.Kp * d_input | ||
self._proportional = 0 | ||
|
||
# clamp error sum to avoid integral windup (and proportional, if proportional-on-measurement is used) | ||
self._error_sum = _clamp(self._error_sum, self.output_limits) | ||
|
||
# compute final output | ||
output = self._proportional + self._error_sum - self.Kd * d_input | ||
output = _clamp(output, self.output_limits) | ||
|
||
# keep track of state | ||
self._last_output = output | ||
self._last_input = input_ | ||
self._last_time = time.time() | ||
|
||
return output | ||
|
||
@property | ||
def tunings(self): | ||
"""The tunings used by the controller as a tuple: (Kp, Ki, Kd)""" | ||
return self.Kp, self.Ki, self.Kd | ||
|
||
@tunings.setter | ||
def tunings(self, tunings): | ||
"""Setter for the PID tunings""" | ||
self.Kp, self.Ki, self.Kd = tunings | ||
|
||
@property | ||
def auto_mode(self): | ||
"""Whether the controller is currently enabled (in auto mode) or not""" | ||
return self._auto_mode | ||
|
||
@auto_mode.setter | ||
def auto_mode(self, enabled): | ||
"""Enable or disable the PID controller""" | ||
if enabled and not self._auto_mode: | ||
# switching from manual mode to auto, reset | ||
self._last_output = None | ||
self._last_input = None | ||
self._error_sum = 0 | ||
self._error_sum = _clamp(self._error_sum, self.output_limits) | ||
|
||
self._auto_mode = enabled | ||
|
||
@property | ||
def output_limits(self): | ||
"""The current output limits as a 2-tuple: (lower, upper). See also the *output_limts* parameter in :meth:`PID.__init__`.""" | ||
return (self._min_output, self._max_output) | ||
|
||
@output_limits.setter | ||
def output_limits(self, limits): | ||
"""Setter for the output limits""" | ||
if limits is None: | ||
self._min_output, self._max_output = None, None | ||
return | ||
|
||
min_output, max_output = limits | ||
|
||
if None not in limits and max_output < min_output: | ||
raise ValueError('lower limit must be less than upper limit') | ||
|
||
self._min_output = min_output | ||
self._max_output = max_output | ||
|
||
self._error_sum = _clamp(self._error_sum, self.output_limits) | ||
self._last_output = _clamp(self._last_output, self.output_limits) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .PID import PID |