Home » PID Controller in Python – How to implement it

PID Controller in Python – How to implement it

Porsche Taycan Turbo

In the domain of control engineering, the PID (Proportional-Integral-Derivative) controller is the place to start. In this post we will see how to implement a PID in Python from scratch.

The example will show you how to implement the Controller in Python to control the speed of a car.

The following picture shows how the theoretical diagram looks like.

PID Controller Diagram

For more information on how a PID Controller works, see PID Controller: The Magic Behind Everyday Systems.


Plant model

The model is a one-dimensional car of mass m, pushed by a force F and subject to aerodynamic drag - \frac{1}{2} C_d A \rho v^2. Where:

  • C_d: drag coefficient
  • A: reference area
  • \rho: air density
  • v: speed

The dynamic equation is:

m \frac{dv}{dt} = F - \frac{1}{2} C_d A \rho v^2

The parameters used are taken from the Porsche Taycan Turbo (Wikipedia):

  • m = 2140 ~ kg
  • C_d A = 0.513 ~ m^2
  • \rho = 1.293 ~ \frac{kg}{m^3}

For convenience \frac{1}{2} C_d A \rho is called b in the model.

Porsche Taycan Turbo

For the Porsche Taycan Turbo Wikipedia reports:

  • Max speed: 260 km/h
  • 0 – 100 km/h: 3.2 s

Based on this info we can assume (approximation) that the maximum force that the electric powertrain can develop depends on the speed (intuitive).

We assume that the dependence is linear as shown in the figure below:

Plot max force

From the max speed we have:

F_{max_{72}} = \frac{1}{2} C_d A \rho v_{max}^2 = 1710 ~ N

where v_{max} = 72 ~ \frac{m}{s} = 260 ~ \frac{km}{h}

Then we can simulate the model in Python to find F_{max_0} that gives 0-100 km/h in 3.2 s. This results in F_{max_0} = 22000 ~ N.



Implementation in Python

The implementation in Python consists of 3 main elements:

  • PID Class
  • Car Class
  • Main Function

The code is available on GitHub: https://github.com/simorxb/PID-Controller-Python.

PID Controller Class

The PID Controller class in Python creates the PID controller object. It is based on several parameters:

  • K_p, K_i, K_d: These are the proportional, integral, and derivative gains, respectively. They determine the response characteristics of the controller.
  • K_{aw}: This is the anti-windup gain, which helps in preventing the integral term from getting too large when the actuator is in saturation.
  • T_C: The time constant for derivative filtering, which smooths out the derivative term.
  • T: The time step, which is the time interval between each step of the controller.
  • max, min: These set the maximum and minimum bounds for the controller output.
  • max_rate: The maximum rate of change (per second) of the controller command.

The __init__ method initializes these parameters, setting the stage for the controller to operate.

The Step function of the PID class is where the action happens. This function takes the current measurement of the process variable and the desired setpoint as inputs.

By calculating the error, updating the integral term with anti-windup correction, and filtering the derivative term, the Step function computes the PID command.

This command is then constrained within set limits and rate of change, ensuring that the output is smooth and controlled.

class PID:

    """ This class implements a PID controller.
    """

    def __init__(self, Kp, Ki, Kd, Kaw, T_C, T, max, min, max_rate):
        self.Kp = Kp                # Proportional gain
        self.Ki = Ki                # Integral gain
        self.Kd = Kd                # Derivative gain
        self.Kaw = Kaw              # Anti-windup gain
        self.T_C = T_C              # Time constant for derivative filtering
        self.T = T                  # Time step
        self.max = max              # Maximum command
        self.min = min              # Minimum command
        self.max_rate = max_rate    # Maximum rate of change of the command
        self.integral = 0           # Integral term
        self.err_prev = 0           # Previous error
        self.deriv_prev = 0         # Previous derivative
        self.command_sat_prev = 0   # Previous saturated command
        self.command_prev = 0       # Previous command
        self.command_sat = 0        # Current saturated command
        self.command = 0            # Current command

    def Step(self, measurement, setpoint):
        """ Execute a step of the PID controller.

        Inputs:
            measurement: current measurement of the process variable
            setpoint: desired value of the process variable
        """

        # Calculate error
        err = setpoint - measurement

        # Update integral term with anti-windup
        self.integral += self.Ki*err*self.T + self.Kaw*(self.command_sat_prev - self.command_prev)*self.T
        
        # Calculate filtered derivative
        deriv_filt = (err - self.err_prev + self.T_C*self.deriv_prev)/(self.T + self.T_C)
        self.err_prev = err
        self.deriv_prev = deriv_filt

        # Calculate command using PID equation
        self.command = self.Kp*err + self.integral + self.Kd*deriv_filt

        # Store previous command
        self.command_prev = self.command

        # Saturate command
        if self.command > self.max:
            self.command_sat = self.max
        elif self.command < self.min:
            self.command_sat = self.min
        else:
            self.command_sat = self.command

        # Apply rate limiter
        if self.command_sat > self.command_sat_prev + self.max_rate*self.T:
            self.command_sat = self.command_sat_prev + self.max_rate*self.T
        elif self.command_sat < self.command_sat_prev - self.max_rate*self.T:
            self.command_sat = self.command_sat_prev - self.max_rate*self.T

        # Store previous saturated command
        self.command_sat_prev = self.command_sat

Car Class

The Car class models a one-dimensional vehicle (as described above). This class has its own set of parameters:

  • m: Mass of the car.
  • b: Aerodynamic drag coefficient.
  • F_max_0, F_max_max: These parameters define the maximum force generated by the powertrain at zero and maximum speed.
  • v_{max}: The maximum speed of the car.
  • T: The time step, which, as in the PID class, is the time interval between each step.

The Step function in the Car class takes an applied force as an input and updates the car’s speed.

It first determines the maximum force generated at the current speed and then saturates the input force if necessary. Following this, the function calculates the rate of change of speed and updates the current speed.

class Car:

    """ This class represents a car moving in 1D, subject to a throttle force F, with mass m, 
        aerodynamic drag coefficient b, F_max/F_min forces, and time step T. 
    """

    def __init__(self, m, b, F_max_0, F_max_max, v_max, T):
        self.m = m                      # Mass of the car
        self.b = b                      # Aerodynamic drag coefficient
        self.F_max_0 = F_max_0          # Max force applied to the car by the powertrain at 0 speed
        self.F_max_max = F_max_max      # Max force applied to the car by the powertrain at max speed
        self.v_max = v_max              # Max speed (m/s)
        self.T = T                      # Time step
        self.v = 0                      # Speed of the car

    def Step(self, F):

        """ Update the speed of the car based on the applied force F.
        """
        # Max force applied by the powertrain depends on the speed
        v_to_F_max_x_axis = [0, self.v_max]
        F_max_y_axis = [self.F_max_0, self.F_max_max]

        if self.v < v_to_F_max_x_axis[0]:
            F_max = F_max_y_axis[0]
        elif self.v > v_to_F_max_x_axis[-1]:
            F_max = F_max_y_axis[-1]
        else:
            F_max = np.interp(self.v, v_to_F_max_x_axis, F_max_y_axis)

        # Saturate input force
        if F > F_max:
            F_sat = F_max

        elif F < 0:
            F_sat = 0
        else:
            F_sat = F

        # Calculate the derivative dv/dt using the input force and the car's speed and properties
        dv_dt = (F_sat - self.b*self.v*self.v)/self.m

        # Update the speed by integrating the derivative using the time step T
        self.v += dv_dt*self.T

Implementing the PID Control Loop

In the main function, we simulate the car’s speed control using the PID controller implemented.

We initialize the PID controller and Car objects with specific parameters:

  • The car’s parameters are initialized with the values corresponding to the Porsche Taycan Turbo, as calculated previously.
  • The PID Controller is initialized with an educated manual guess.

We then enter a loop that represents our simulation. At each step, we calculate the car’s current speed and use the PID controller to compute the force needed to reach a predefined setpoint speed (42 m/s). This force is then applied to the car, and the speed is updated accordingly.

Lastly, we plot the car’s speed response and the force command over time to visualize the control system’s performance:

Simulation result
import numpy as np
from lib import Car, PID
import matplotlib.pyplot as plt

def main():
    # -------- Configuration --------

    # Simulation parameters

    time_step = 0.1
    end_time = 25
    length = round(end_time/time_step)

    t = np.zeros(length)
    stp = np.zeros(length)
    v = np.zeros(length)
    command = np.zeros(length)

    # Car parameters

    m = 2140
    b = 0.33
    F_max_0 = 22000
    F_max_max = 1710
    v_max = 72

    # PID parameters

    Kp = 800.0
    Ki = 70.0
    Kaw = 1.0
    Kd = 20.0
    T_C = 1.0

    # Initialize PID controller
    pid = PID(Kp, Ki, Kd, Kaw, T_C, time_step, F_max_0, 0, 30000)

    # Initialize car with given parameters
    car = Car(m, b, F_max_0, F_max_max, v_max, time_step)

    # Iterate through time steps
    for idx in range(0, length):
        t[idx] = idx*time_step
        # Set setpoint
        stp[idx] = 42
        
        # Execute the control loop
        v[idx] = car.v
        pid.Step(v[idx], stp[idx])
        command[idx] = pid.command_sat
        car.Step(command[idx])  

    # Plot speed response

    plt.subplot(2, 1, 1)
    plt.plot(t, v, label="Response")
    plt.plot(t, stp, '--', label="Setpoint")
    plt.xlabel("Time [s]")
    plt.ylabel("Speed [m/s]")
    plt.legend()
    plt.grid()

    # Plot command force

    plt.subplot(2, 1, 2)
    plt.plot(t, command, label="Command")
    plt.xlabel("Time [s]")
    plt.ylabel("Force [N]")
    plt.legend()
    plt.grid()

    # Display the plots

    plt.show()

main()


Conclusion

Implementing a PID controller in Python can be useful whether you need to run a simulation or you are dealing with the real-world using a microcontroller based on Python. With this Python-based PID controller, you can control not just virtual cars but potentially any process.

Remember, the PID controller is a versatile tool, and while this example is automotive-focused, the principles remain the same across a multitude of applications.

Happy control engineering!

Feel free to ask any questions in the comments, and if you want to dive deeper into the world of PID Controllers here’s my video course:

PID Control – Things I Wish I Knew When I Graduated

6 thoughts on “PID Controller in Python – How to implement it”

  1. Hello Simone,

    Can you give me coupon to enroll in your PID Control course? I click the link but it is not applying the 25% off. Also, I read the pre-req’s for the course. I have been out of school for a while now. Do not remember Laplace transform. Do you have some reading materials prior that I can work towards prior to starting the course? Thank you

    1. Hi quoit1,

      Apologies for the discount, my bad, if you try again the 25% off will be applied. Please let me know if that’s not the case.

      Regarding the Laplace transform I don’t have anything to suggest on the spot, but I can look around.

      If you have any book on control engineering that should give a good enough intro to the Laplace transform.

    1. Hi Waleed, of course, you can use it. For the reference you can simply mention: “PID Controller in Python – How to implement it”, published by Simone Bertoni on simonebertonilab.com. Is that okay?

  2. Hi Simone,
    I am now working on the PID controller system. But I have no idea how to run the PID python code. Do you have any tutorial how to use it? I try copy the codes on to the python Idle, but it is not working.
    Thanks so much

    1. Hi David, thanks for your interest.

      I think you want to use a more advanced IDE than IDLE, for example VS Code. You could follow this tutorial to create a folder with a virtual environment:

      https://code.visualstudio.com/docs/python/python-tutorial

      Then install matplotlib and numpy in the venv using “pip install ” and copy the two files main.py and lib.py from https://github.com/simorxb/PID-Controller-Python

      At this point you should be able to run main.py

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Menu