Proportional-Integral-Derivative (PID) controllers are widely used in industrial control systems to maintain a desired output value by adjusting a control input. A PID controller continuously calculates an error value as the difference between a desired setpoint and a measured process variable. It then applies a correction based on proportional, integral, and derivative terms.
This article focuses on the implementation of a positional PID control algorithm using Python. The discrete form of the positional PID algorithm for calculating the control output at each step is given by:
Output(k) = Kp * Error(k) + Ki * Sum(Error(i) for i=0 to k) + Kd * (Error(k) - Error(k-1))
Where:
Output(k)is the control output at the current time stepk.Error(k)is the difference between the setpoint and the current process variable at stepk.Kp,Ki, andKdare the proportional, integral, and derivative gains, respectively.Sum(Error(i) for i=0 to k)represents the accumulated error over time.(Error(k) - Error(k-1))represents the rate of change of the error.
Below is a Python class that encapsulates the logic for a positional PID controller.
class PIDController:
"""
A class implementing a positional PID controller.
"""
def __init__(self, Kp_gain=0.0, Ki_gain=0.0, Kd_gain=0.0, initial_process_value=0.0):
self.Kp = Kp_gain
self.Ki = Ki_gain
self.Kd = Kd_gain
self.process_variable = initial_process_value
self.cumulative_error = 0.0
self.previous_error = 0.0
def update(self, setpoint):
"""
Calculates and applies the control output for one time step.
Args:
setpoint (float): The desired target value.
Returns:
float: The updated process variable after applying the control output.
"""
# Calculate the current error
current_error = setpoint - self.process_variable
# Update the integral sum of errors
self.cumulative_error += current_error
# Calculate the derivative of the error
derivative_error = current_error - self.previous_error
# Compute the PID control output
control_output = (self.Kp * current_error) + \
(self.Ki * self.cumulative_error) + \
(self.Kd * derivative_error)
# Update the process variable based on the control output
self.process_variable += control_output
# Store the current error to become the previous error for the next iteration
self.previous_error = current_error
return self.process_variable
To visualize the behavior of the PID controller, a simulation function is useful. This function will iteratively call the update method of the PIDController and plot the process variable's response over time. For this, we'll need the numpy library for numerical operations and matplotlib for plotting.
import numpy as np
import matplotlib.pyplot as plt
The simulation function run_pid_simulation takes the PID gains, an initial state, the total simulation duration (in steps), and the target setpoint as input. It then instantiates the PIDController, collects data at each step, and finally generates a plot.
def run_pid_simulation(proportional_gain=0.0, integral_gain=0.0, derivative_gain=0.0,
initial_state=0.0, simulation_steps=1, target_setpoint=0.0):
"""
Simulates a PID controller's response and plots the results.
Args:
proportional_gain (float): Kp gain for the PID controller.
integral_gain (float): Ki gain for the PID controller.
derivative_gain (float): Kd gain for the PID controller.
initial_state (float): The starting value of the process variable.
simulation_steps (int): The number of time steps to simulate.
target_setpoint (float): The desired target value for the process variable.
"""
controller = PIDController(proportional_gain, integral_gain, derivative_gain, initial_state)
# Lists to store the process variable's history and corresponding time points
process_variable_history = []
time_steps = []
process_variable_history.append(controller.process_variable)
time_steps.append(0)
# Run the simulation loop
for step_num in range(1, simulation_steps + 1):
current_pv = controller.update(target_setpoint)
process_variable_history.append(current_pv)
time_steps.append(step_num)
# Convert lists to NumPy arrays for efficient plotting
np_time = np.array(time_steps)
np_pv_history = np.array(process_variable_history)
# Plotting the results
plt.figure(figsize=(10, 6))
plt.style.use('seaborn-v0_8-darkgrid')
plt.plot(np_time, np_pv_history, label='Process Variable', color='blue', linewidth=2)
plt.axhline(target_setpoint, color='red', linestyle='--', label='Target Setpoint', linewidth=1.5)
plt.xlim((0, simulation_steps))
# Dynamic Y-axis limits with a small buffer
min_val = np.min(np_pv_history)
max_val = np.max(np_pv_history)
buffer = (max_val - min_val) * 0.1 if (max_val - min_val) != 0 else 1.0
plt.ylim((min_val - buffer, max_val + buffer))
plt.xlabel('Time Step')
plt.ylabel('Process Value')
plt.title('PID Controller Response Simulation')
plt.legend()
plt.grid(True)
plt.show()
Putting it all together, here is a complete script demonstrating the positional PID controller and its simulation.
import numpy as np
import matplotlib.pyplot as plt
class PIDController:
"""
A class implementing a positional PID controller.
"""
def __init__(self, Kp_gain=0.0, Ki_gain=0.0, Kd_gain=0.0, initial_process_value=0.0):
self.Kp = Kp_gain
self.Ki = Ki_gain
self.Kd = Kd_gain
self.process_variable = initial_process_value
self.cumulative_error = 0.0
self.previous_error = 0.0
def update(self, setpoint):
"""
Calculates and applies the control output for one time step.
Args:
setpoint (float): The desired target value.
Returns:
float: The updated process variable after applying the control output.
"""
current_error = setpoint - self.process_variable
self.cumulative_error += current_error
derivative_error = current_error - self.previous_error
control_output = (self.Kp * current_error) + \
(self.Ki * self.cumulative_error) + \
(self.Kd * derivative_error)
self.process_variable += control_output
self.previous_error = current_error
return self.process_variable
def run_pid_simulation(proportional_gain=0.0, integral_gain=0.0, derivative_gain=0.0,
initial_state=0.0, simulation_steps=1, target_setpoint=0.0):
"""
Simulates a PID controller's response and plots the results.
Args:
proportional_gain (float): Kp gain for the PID controller.
integral_gain (float): Ki gain for the PID controller.
derivative_gain (float): Kd gain for the PID controller.
initial_state (float): The starting value of the process variable.
simulation_steps (int): The number of time steps to simulate.
target_setpoint (float): The desired target value for the process variable.
"""
controller = PIDController(proportional_gain, integral_gain, derivative_gain, initial_state)
process_variable_history = []
time_steps = []
process_variable_history.append(controller.process_variable)
time_steps.append(0)
for step_num in range(1, simulation_steps + 1):
current_pv = controller.update(target_setpoint)
process_variable_history.append(current_pv)
time_steps.append(step_num)
np_time = np.array(time_steps)
np_pv_history = np.array(process_variable_history)
plt.figure(figsize=(10, 6))
plt.style.use('seaborn-v0_8-darkgrid')
plt.plot(np_time, np_pv_history, label='Process Variable', color='blue', linewidth=2)
plt.axhline(target_setpoint, color='red', linestyle='--', label='Target Setpoint', linewidth=1.5)
plt.xlim((0, simulation_steps))
min_val = np.min(np_pv_history)
max_val = np.max(np_pv_history)
buffer = (max_val - min_val) * 0.1 if (max_val - min_val) != 0 else 1.0
plt.ylim((min_val - buffer, max_val + buffer))
plt.xlabel('Time Step')
plt.ylabel('Process Value')
plt.title('PID Controller Response Simulation')
plt.legend()
plt.grid(True)
plt.show()
if __name__ == '__main__':
# Example usage: simulate PID with specific gains, starting at 0, for 200 steps, targeting 100.
run_pid_simulation(proportional_gain=0.1, integral_gain=0.1, derivative_gain=0.1,
initial_state=0, simulation_steps=200, target_setpoint=100)