Skip to content

Instantly share code, notes, and snippets.

@deekb
Created July 9, 2023 12:18
Show Gist options
  • Save deekb/7aef5838d73dbfcd6b77de2072694791 to your computer and use it in GitHub Desktop.
Save deekb/7aef5838d73dbfcd6b77de2072694791 to your computer and use it in GitHub Desktop.
A simple, tunable PID with instructions written in python
"""
PID can be confusing, it's okay if you don't understand it and still want to use it,
I hope this mini-guide gives you enough information on PID to start using it in your own projects:
PID Explained:
PID (Proportional-Integral-Derivative) is a control algorithm used in engineering and robotics.
It adjusts its output based on the error signal, which represents the deviation from the desired setpoint.
The gains, namely Kp, Ki, and Kd, determine the contribution of each component.
PID gains are prefixed with a "K" to indicate that they are coefficients or constants used to scale the effect of each component.
Such effects are explained below, as well as a guide on tuning their gains.
Kp is the Proportional gain, it determines the response of the control system based on the current error.
It is directly proportional to the error, meaning a higher value of Kp will result in a stronger response to the error.
However, if Kp is set too high, it can lead to overshooting and instability or oscillation.
Ki: is the Integral gain, it accounts for the accumulated error over time and helps eliminate steady-state errors.
It integrates the error signal and produces a control output that reduces the cumulative error.
A higher value of Ki increases the response to long-term error, but it can also introduce oscillations and instability if set too high.
Kd: is the Derivative gain, it is responsible for anticipating future error trends by analyzing the rate of change of the error signal.
It provides a damping effect by reducing the response as the error approaches zero.
A higher value of Kd helps to stabilize the system and improve response time, but excessive values can cause high-frequency noise amplification and instability.
Tuning a PID controller:
Tuning a PID controller involves adjusting the values of Kp, Ki, and Kd, also called gains,
in order to achieve the desired system response. Here are some general guidelines for tuning a PID controller:
1. Start by setting all gains to zero.
2. Increase the value of Kp slowly (start in increments of 0.05) until the system starts to respond quickly to changes but without excessive oscillations or overshoot.
3. Add a small value of Ki to eliminate steady-state errors (when the system has stabilized but has not yet reached the setpoint) and bring the system to the desired setpoint.
4. Adjust Kd to dampen oscillations and improve system stability without introducing excessive noise.
5. Repeat steps 2-4, fine-tuning each gain until the system response meets the desired requirements.
6. It may also be necessary to consider additional advanced tuning techniques such as Ziegler-Nichols or trial-and-error methods for more complex systems.
"""
class PIDController(object):
"""
A generalized PID controller implementation.
"""
def __init__(self, timer: Brain.timer, kp: float = 1.0, ki: float = 0.0, kd: float = 0.0, t: float = 0.05, integral_limit: float = 1.0):
"""
Initializes a PIDController instance.
:param timer: The timer object used to measure time.
:param kp: Kp value for the PID.
:param ki: Ki value for the PID.
:param kd: Kd value for the PID.
:param t: Minimum time between update calls. All calls made before this amount of time has passed will be ignored.
:param integral_limit: The maximum absolute value for the integral term to prevent windup.
"""
self._kp = kp
self._ki = ki
self._kd = kd
self._timer = timer
self._time_step = t
self._previous_time = timer.time(MSEC) / 1000
self._current_value = 0.0
self._target_value = 0.0
self._error_integral = 0.0
self._integral_limit = integral_limit
self._previous_error = 0.0
self._control_output = 0.0
@property
def kp(self) -> float:
"""
Getter for the Kp value of the PID.
:return: The Kp value.
"""
return self._kp
@kp.setter
def kp(self, value: float):
"""
Setter for the Kp value of the PID.
:param value: The new Kp value.
"""
self._kp = value
@property
def ki(self) -> float:
"""
Getter for the Ki value of the PID.
:return: The Ki value.
"""
return self._ki
@ki.setter
def ki(self, value: float):
"""
Setter for the Ki value of the PID.
:param value: The new Ki value.
"""
self._ki = value
@property
def kd(self) -> float:
"""
Getter for the Kd value of the PID.
:return: The Kd value.
"""
return self._kd
@kd.setter
def kd(self, value: float):
"""
Setter for the Kd value of the PID.
:param value: The new Kd value.
"""
self._kd = value
@property
def target_value(self) -> float:
"""
Getter for the target value of the PID.
:return: The target value.
"""
return self._target_value
@target_value.setter
def target_value(self, value: float):
"""
Setter for the target value of the PID.
:param value: The new target value.
"""
self._target_value = value
self._error_integral = 0
self._previous_error = self._target_value - self._current_value
self._control_output = 0
def update(self, current_value: float) -> float:
"""
Update the PID state with the most recent current value and calculate the control output.
:param current_value: The current measurement or feedback value.
:return: The calculated control output.
"""
current_time = self._timer.time(MSEC) / 1000
delta_time = current_time - self._previous_time
if delta_time < self._time_step:
return self._control_output
self._previous_time = current_time
current_error = self._target_value - current_value
self._error_integral += current_error * delta_time
# Apply integral windup prevention
# PID integral windup is a phenomenon that occurs when the integral term of a PID
# controller continues to accumulate error even when the controller's output is saturated.
# This can lead to overshoot, instability, and poor performance in control systems.
# if your Kp is reasonably low, and you are still experiencing overshoot/instability/oscillation,
# then try decreasing the integral limit
if self._ki != 0:
self._error_integral = clamp(self._error_integral, -self._integral_limit / self._ki, self._integral_limit / self._ki)
error_derivative = (current_error - self._previous_error) / delta_time
self._control_output = (
self._kp * current_error + self._ki * self._error_integral + self._kd * error_derivative
)
self._previous_error = current_error
return self._control_output
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment