Implementing a Positional PID Controller in Python

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 step k.
  • Error(k) is the difference between the setpoint and the current process variable at step k.
  • Kp, Ki, and Kd are 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)

Tags: PID control python Control Systems algorithm simulation

Posted on Fri, 29 May 2026 23:46:45 +0000 by scifo