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.
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
ubased on the errore = 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:
dtis how often the controller updates — smaller values give finer control.setpointis where we want the cart to end up.Kp,Ki,Kdare the three gains you will tune in later steps.- Start with only
Kpenabled and addKi,Kdgradually.
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:
Preacts to the current error — larger error gives a stronger push.Iaccumulates the past error — useful to remove steady-state offset.Dpredicts future error from how fast it’s changing — damps oscillations.prev_errorandintegralare 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:
| Step | Code Part | Purpose |
|---|---|---|
| 1 | pid.compute(...) | Reads error, returns the control output |
| 2 | x = x + u * dt | Applies the control to the simulated cart |
| 3 | Append to lists | Records 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.
| Setting | Kp | Ki | Kd | What you should see |
|---|---|---|---|---|
| P only (low) | 0.5 | 0 | 0 | Slow, never quite reaches setpoint |
| P only (high) | 5.0 | 0 | 0 | Fast but overshoots and oscillates |
| PI | 1.0 | 0.5 | 0 | Removes steady-state error but adds oscillation |
| PD | 2.0 | 0 | 0.5 | Fast rise with damping, small offset |
| PID | 2.0 | 0.5 | 0.5 | Fast, smooth, accurate — the goal |
Tip : Tune
Kpfirst, then addKdto reduce overshoot, and finallyKito remove any remaining steady-state error. This is the classic manual tuning order.
Step 7: Exercise
Try These:
- Change the setpoint mid-simulation (e.g. switch from
10to5att=10s) and see how the controller reacts. - Add a disturbance — apply a constant
-2force for a few timesteps and watch the controller recover. - 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. - 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.