Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PID From Theory to Application

In the previous section, we learned the core ideas behind a PID controller — what proportional, integral, and derivative terms do, and how the control signal u(t) is computed from the error e(t) = r(t) - y(t).

Now, we’ll move beyond the theory and explore how a PID controller actually works in practice — by implementing it in code and applying it to a simple 1D position control problem. You’ll see how each gain (Kp, Ki, Kd) shapes the system’s response and how to tune them to reach the setpoint smoothly.


Watch the Introduction

Here are some short explainer videos to help you visualize PID behavior before we dive into the code.

Video 1: PID Controller Explained

Video 2: PID Tuning Walkthrough

Also you can refer to this article for an in-depth guide on tuning PID controllers in real systems.


Section 2: PID in Action

In this section we will be looking at an example of PID control on a simple 1D system — a point mass being driven to a target position.

Step 1: Understanding the Problem

Imagine a cart on a 1D track.

  • The cart starts at position x = 0
  • The goal (setpoint) is to reach x = 10
  • At every timestep, the controller computes a force u based on the error e = setpoint - x
  • The cart’s motion is updated using simple physics: x_new = x + u * dt
  • We want the cart to reach the setpoint quickly, smoothly, and without overshooting

You can visualize the loop as:

setpoint ----> ( + ) ----> [ PID ] ----> [ Cart ] ----> position
                  ^                                         |
                  |_________________________________________|
                                  feedback

Step 2: Define System Parameters

import numpy as np
import matplotlib.pyplot as plt

# Simulation
dt = 0.1            # Timestep (seconds)
total_time = 20.0   # Total simulation time
steps = int(total_time / dt)

# System
setpoint = 10.0     # Target position
x = 0.0             # Initial position

# PID gains (tune these!)
Kp = 1.0
Ki = 0.0
Kd = 0.0

Explanation:

  • dt is how often the controller updates — smaller values give finer control.
  • setpoint is where we want the cart to end up.
  • Kp, Ki, Kd are the three gains you will tune in later steps.
  • Start with only Kp enabled and add Ki, Kd gradually.

Step 3: Implement the PID Controller

class PID:
    def __init__(self, Kp, Ki, Kd):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.prev_error = 0.0
        self.integral = 0.0

    def compute(self, setpoint, measured, dt):
        error = setpoint - measured

        # Proportional term
        P = self.Kp * error

        # Integral term (accumulates past error)
        self.integral += error * dt
        I = self.Ki * self.integral

        # Derivative term (rate of change of error)
        derivative = (error - self.prev_error) / dt
        D = self.Kd * derivative

        self.prev_error = error
        return P + I + D

Explanation:

  • P reacts to the current error — larger error gives a stronger push.
  • I accumulates the past error — useful to remove steady-state offset.
  • D predicts future error from how fast it’s changing — damps oscillations.
  • prev_error and integral are stored between calls so the controller has memory.

Step 4: Run the Simulation

pid = PID(Kp, Ki, Kd)

positions = []
time_log = []

for step in range(steps):
    # 1. Compute control signal
    u = pid.compute(setpoint, x, dt)

    # 2. Apply control to the cart (very simple model)
    x = x + u * dt

    # 3. Log data for plotting
    positions.append(x)
    time_log.append(step * dt)

Explanation:

StepCode PartPurpose
1pid.compute(...)Reads error, returns the control output
2x = x + u * dtApplies the control to the simulated cart
3Append to listsRecords position over time for plotting

Step 5: Visualize the Response

plt.plot(time_log, positions, label="Cart position")
plt.axhline(setpoint, color='r', linestyle='--', label="Setpoint")
plt.xlabel("Time (s)")
plt.ylabel("Position")
plt.title(f"PID Response (Kp={Kp}, Ki={Ki}, Kd={Kd})")
plt.legend()
plt.grid(True)
plt.show()

Explanation:

  • The red dashed line is the target position.
  • The blue curve shows how the cart actually moves over time.
  • A good controller’s curve should rise quickly, settle near the setpoint, and not oscillate.

Step 6: Tuning the Gains

Try these combinations one at a time and observe the plot.

SettingKpKiKdWhat you should see
P only (low)0.500Slow, never quite reaches setpoint
P only (high)5.000Fast but overshoots and oscillates
PI1.00.50Removes steady-state error but adds oscillation
PD2.000.5Fast rise with damping, small offset
PID2.00.50.5Fast, smooth, accurate — the goal

Tip : Tune Kp first, then add Kd to reduce overshoot, and finally Ki to remove any remaining steady-state error. This is the classic manual tuning order.


Step 7: Exercise

Try These:

  1. Change the setpoint mid-simulation (e.g. switch from 10 to 5 at t=10s) and see how the controller reacts.
  2. Add a disturbance — apply a constant -2 force for a few timesteps and watch the controller recover.
  3. Replace the simple cart model with one that includes mass and friction: x_new = x + v*dt; v_new = v + (u - 0.5*v)/mass * dt.
  4. Plot the error over time alongside the position to visualize convergence.

Bonus Challenge: Implement an anti-windup mechanism that caps self.integral to a maximum value, and observe how it prevents the controller from going crazy when the error stays large for a long time.