Introduction
PID control is the most widely used feedback control method in industry. It appears in temperature control, motor control, process control, and countless other applications.
For the theoretical foundations of PID control, see Fundamentals of PID Control and the Role of Each Component. In this article, we implement a PID controller in Python, simulate its behavior with a first-order system, compare P/PI/PID step responses, and introduce the classical Ziegler-Nichols tuning method.
Implementing a Discrete PID Controller
The continuous-time PID control law is expressed as:
\[ u(t) = K_P e(t) + K_I \int_0^t e(\tau)d\tau + K_D \frac{de(t)}{dt} \]where \(e(t) = r(t) - y(t)\) is the error between the setpoint and the output. For computer implementation, we approximate the integral with a cumulative sum and the derivative with a backward difference, yielding the discrete PID control law:
\[ u[k] = K_P e[k] + K_I \Delta t \sum_{i=0}^{k} e[i] + K_D \frac{e[k] - e[k-1]}{\Delta t} \tag{1} \]where \(\Delta t\) is the sampling period.
The following Python implementation includes output limiting and anti-windup.
class PIDController:
def __init__(self, kp, ki, kd, dt, output_limits=None):
self.kp = kp
self.ki = ki
self.kd = kd
self.dt = dt
self.output_limits = output_limits
self.integral = 0.0
self.prev_error = 0.0
def update(self, setpoint, measured):
error = setpoint - measured
# Proportional term
p_term = self.kp * error
# Integral term (cumulative sum approximation)
self.integral += error * self.dt
i_term = self.ki * self.integral
# Derivative term (backward difference approximation)
derivative = (error - self.prev_error) / self.dt
d_term = self.kd * derivative
self.prev_error = error
output = p_term + i_term + d_term
# Output limiting (anti-windup)
if self.output_limits is not None:
lo, hi = self.output_limits
if output > hi:
output = hi
self.integral -= error * self.dt
elif output < lo:
output = lo
self.integral -= error * self.dt
return output
def reset(self):
self.integral = 0.0
self.prev_error = 0.0
The anti-windup mechanism stops the integral term from accumulating when the output reaches its limits. Without it, the integral value would grow unboundedly during saturation, causing a large overshoot when the constraint is released.
Plant Model
We use a first-order system as the plant for simulation. Its transfer function is:
\[ G(s) = \frac{K}{1 + Ts} \tag{2} \]where \(K\) is the process gain and \(T\) is the time constant. Discretizing this differential equation gives the following update rule:
\[ y[k+1] = y[k] + \frac{\Delta t}{T}(K \cdot u[k] - y[k]) \tag{3} \]The Python implementation is as follows.
class FirstOrderSystem:
def __init__(self, gain, time_constant, dt):
self.gain = gain
self.time_constant = time_constant
self.dt = dt
self.y = 0.0
def update(self, u):
self.y += self.dt / self.time_constant * (self.gain * u - self.y)
return self.y
def reset(self):
self.y = 0.0
Comparing P, PI, and PID Step Responses
We compare the step responses of three controller types. The plant has \(K=1.0\) and \(T=1.0\).
import numpy as np
import matplotlib.pyplot as plt
dt = 0.01
t_end = 10.0
t = np.arange(0, t_end, dt)
setpoint = np.ones_like(t) # Step input
plant_params = {'gain': 1.0, 'time_constant': 1.0, 'dt': dt}
configs = [
('P control (Kp=2.0)', {'kp': 2.0, 'ki': 0.0, 'kd': 0.0}),
('PI control (Kp=2.0, Ki=1.0)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.0}),
('PID control (Kp=2.0, Ki=1.0, Kd=0.5)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.5}),
]
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for label, params in configs:
pid = PIDController(**params, dt=dt)
plant = FirstOrderSystem(**plant_params)
y_hist = []
u_hist = []
for sp in setpoint:
u = pid.update(sp, plant.y)
y = plant.update(u)
y_hist.append(y)
u_hist.append(u)
axes[0].plot(t, y_hist, label=label)
axes[1].plot(t, u_hist, label=label)
axes[0].axhline(y=1.0, color='k', linestyle='--', alpha=0.5, label='Setpoint')
axes[0].set_ylabel('Output y(t)')
axes[0].set_title('Step Response Comparison')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].set_ylabel('Control input u(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
From the simulation results, the following characteristics can be observed:
- P control: The response is fast, but a steady-state error remains. With \(K_P = 2.0\), the theoretical steady-state error is \(\frac{1}{1 + K \cdot K_P} = \frac{1}{3} \approx 0.33\).
- PI control: The integral term eliminates the steady-state error. However, overshoot tends to increase.
- PID control: The derivative term suppresses overshoot, achieving both the fast response of P control and the steady-state accuracy of PI control.
Effect of Gain Parameters
The setting of each gain parameter significantly affects control performance.
Excessive Proportional Gain \(K_P\)
Setting \(K_P\) too large makes the response oscillatory and eventually unstable. For example, with P control at \(K_P = 10.0\), the output oscillates around the setpoint.
Integral Gain \(K_I\) and Windup
When \(K_I\) is large or the control output is constrained, the integral term can accumulate excessively, a phenomenon called “windup.” While the output is saturated, the integral value keeps growing, causing a large overshoot after the constraint is released. The anti-windup mechanism described earlier mitigates this problem.
Derivative Gain \(K_D\) and Noise Sensitivity
Since the derivative term uses the rate of change of the error, it is sensitive to measurement noise. Setting a large \(K_D\) on a noisy signal causes rapid fluctuations in the control output. In practice, a low-pass filter on the derivative input, known as “filtered derivative,” is commonly used.
Parameter Tuning Guidelines
| Parameter | Effect of Increasing | Caution |
|---|---|---|
| \(K_P\) | Faster response, reduced steady-state error | Oscillation and instability if too large |
| \(K_I\) | Eliminates steady-state error | Increased overshoot, windup |
| \(K_D\) | Suppresses overshoot, improves response | Sensitive to noise |
Ziegler-Nichols Tuning Method
The Ziegler-Nichols method is a classical approach for empirically determining PID gains from the plant’s step response characteristics. Here we introduce the step response method (open-loop method).
Step Response Method
A step input is applied to the plant, and the following three parameters are read from the response curve:
- \(K\): Process gain (steady-state value / input value)
- \(L\): Dead time (delay before the response begins)
- \(T\): Time constant (time from the inflection point tangent to reaching the steady-state value)
Based on these parameters, PID gains are calculated using the following table.
| Controller | \(K_P\) | \(T_I\) | \(T_D\) |
|---|---|---|---|
| P | \(\frac{T}{KL}\) | - | - |
| PI | \(\frac{0.9T}{KL}\) | \(\frac{L}{0.3}\) | - |
| PID | \(\frac{1.2T}{KL}\) | \(2L\) | \(0.5L\) |
Here, \(T_I\) is the integral time and \(T_D\) is the derivative time, with the relationships \(K_I = K_P / T_I\) and \(K_D = K_P \cdot T_D\).
The Ziegler-Nichols method tends to produce oscillatory responses as its starting point, so the resulting gains often lead to significant overshoot. In practice, these values are used as initial settings and then fine-tuned manually.
Disturbance Rejection Simulation
In real control systems, disturbances (unexpected external inputs) affect control performance. Here we add a step disturbance midway through the simulation and compare the disturbance rejection of P control and PID control.
import numpy as np
import matplotlib.pyplot as plt
dt = 0.01
t_end = 20.0
t = np.arange(0, t_end, dt)
setpoint = np.ones_like(t)
# Disturbance: step of magnitude 0.5 at t=10s
disturbance = np.zeros_like(t)
disturbance[t >= 10.0] = 0.5
plant_params = {'gain': 1.0, 'time_constant': 1.0, 'dt': dt}
configs = [
('P control (Kp=2.0)', {'kp': 2.0, 'ki': 0.0, 'kd': 0.0}),
('PID control (Kp=2.0, Ki=1.0, Kd=0.5)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.5}),
]
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for label, params in configs:
pid = PIDController(**params, dt=dt)
plant = FirstOrderSystem(**plant_params)
y_hist = []
u_hist = []
for i, sp in enumerate(setpoint):
u = pid.update(sp, plant.y)
# Add disturbance to plant input
y = plant.update(u + disturbance[i])
y_hist.append(y)
u_hist.append(u)
axes[0].plot(t, y_hist, label=label)
axes[1].plot(t, u_hist, label=label)
axes[0].axhline(y=1.0, color='k', linestyle='--', alpha=0.5, label='Setpoint')
axes[0].axvline(x=10.0, color='r', linestyle=':', alpha=0.5, label='Disturbance onset')
axes[0].set_ylabel('Output y(t)')
axes[0].set_title('Disturbance Rejection Comparison')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].set_ylabel('Control input u(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
The simulation results show the following:
- P control: It provides some disturbance suppression, but a steady-state error due to the disturbance remains.
- PID control: Thanks to the integral term, the output eventually returns to the setpoint even after the disturbance is applied. The derivative term also improves the initial response to the disturbance.
Summary
In this article, we implemented a PID controller in Python and verified its behavior through simulation with a first-order system. By comparing P, PI, and PID step responses, we experimentally confirmed the role and effect of each component.
PID control is simple in structure yet achieves high control performance with proper tuning. However, for systems with strong nonlinearity or multiple inputs and outputs, more advanced control methods become necessary.
Related Articles
- Fundamentals of PID Control and the Role of Each Component - Explains the mathematical definitions of P, PI, and PID control and the characteristics of each component.
- Mathematics of MPPI (Model Predictive Path Integral) - Introduces model predictive control based on Monte Carlo sampling as an advanced control method beyond PID.
- Kalman Filter: Theory and Python Implementation - Explains state estimation methods used in combination with feedback control.
- Fundamentals of Filtering Methods in Signal Processing - Provides an overview of filtering methods used for state estimation in control systems.
- Matplotlib Practical Tips: Creating Publication-Quality Plots - Introduces settings for creating higher-quality plots of simulation results.
References
- Astrom, K. J., & Murray, R. M. (2021). Feedback Systems: An Introduction for Scientists and Engineers (2nd ed.). Princeton University Press.
- Ziegler, J. G., & Nichols, N. B. (1942). “Optimum settings for automatic controllers”. Transactions of the ASME, 64(11), 759-768.