Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
m-lundberg committed May 11, 2018
0 parents commit 3f89198
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
venv/
docs/build
__pycache__/
138 changes: 138 additions & 0 deletions simple_pid/PID.py
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)
1 change: 1 addition & 0 deletions simple_pid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .PID import PID

0 comments on commit 3f89198

Please sign in to comment.