Chapter 2: PID Control Fundamentals

What is PID Control?

PID stands for Proportional-Integral-Derivative. It's the most widely used control algorithm in industry.

u(t)=Kpe(t)+Ki∫0te(Ļ„)dĻ„+Kdde(t)dtu(t) = K_p e(t) + K_i \int_0^t e(\tau)d\tau + K_d \frac{de(t)}{dt}

Where:

  • e(t) = error = setpoint - measured value
  • Kp = proportional gain
  • Ki = integral gain
  • Kd = derivative gain

The Three Components

Proportional (P) Control

uP(t)=Kpā‹…e(t)u_P(t) = K_p \cdot e(t)

  • Responds proportionally to current error
  • Higher Kp → faster response, but more oscillation
  • Cannot eliminate steady-state error
class PController:
    def __init__(self, Kp):
        self.Kp = Kp
    
    def compute(self, setpoint, measured):
        error = setpoint - measured
        return self.Kp * error

Integral (I) Control

uI(t)=Ki∫0te(Ļ„)dĻ„u_I(t) = K_i \int_0^t e(\tau)d\tau

  • Accumulates past errors
  • Eliminates steady-state error
  • Can cause overshoot and oscillation
class PIController:
    def __init__(self, Kp, Ki, dt):
        self.Kp = Kp
        self.Ki = Ki
        self.dt = dt
        self.integral = 0
    
    def compute(self, setpoint, measured):
        error = setpoint - measured
        self.integral += error * self.dt
        return self.Kp * error + self.Ki * self.integral

Derivative (D) Control

uD(t)=Kdde(t)dtu_D(t) = K_d \frac{de(t)}{dt}

  • Predicts future error based on rate of change
  • Reduces overshoot
  • Sensitive to noise
class PIDController:
    def __init__(self, Kp, Ki, Kd, dt):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.dt = dt
        self.integral = 0
        self.prev_error = 0
    
    def compute(self, setpoint, measured):
        error = setpoint - measured
        
        # Proportional
        P = self.Kp * error
        
        # Integral
        self.integral += error * self.dt
        I = self.Ki * self.integral
        
        # Derivative
        derivative = (error - self.prev_error) / self.dt
        D = self.Kd * derivative
        
        self.prev_error = error
        
        return P + I + D

Effect of Each Term

Term Response Speed Overshoot Steady-State Error Stability
↑ Kp Faster Increases Decreases (not zero) Decreases
↑ Ki Slower Increases Eliminates Decreases
↑ Kd Faster Decreases No effect Improves

Complete PID Implementation

import numpy as np

class PID:
    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
        self.prev_error = 0
        self.prev_derivative = 0
        
    def reset(self):
        self.integral = 0
        self.prev_error = 0
        
    def compute(self, setpoint, measured):
        error = setpoint - measured
        
        # Proportional term
        P = self.Kp * error
        
        # Integral term with anti-windup
        self.integral += error * self.dt
        I = self.Ki * self.integral
        
        # Derivative term (on measurement to avoid derivative kick)
        derivative = (error - self.prev_error) / self.dt
        # Low-pass filter on derivative
        alpha = 0.1
        filtered_derivative = alpha * derivative + (1 - alpha) * self.prev_derivative
        D = self.Kd * filtered_derivative
        
        self.prev_error = error
        self.prev_derivative = filtered_derivative
        
        output = P + I + D
        
        # Apply output limits
        if self.output_limits:
            output = np.clip(output, self.output_limits[0], self.output_limits[1])
            # Anti-windup: stop integrating if saturated
            if output == self.output_limits[0] or output == self.output_limits[1]:
                self.integral -= error * self.dt
        
        return output

Simulation Example

import numpy as np
import matplotlib.pyplot as plt

# Simulate a first-order system with PID control
def simulate_pid():
    dt = 0.01
    time = np.arange(0, 10, dt)
    
    # System: first-order with time constant tau
    tau = 1.0
    
    # PID controller
    pid = PID(Kp=2.0, Ki=1.0, Kd=0.5, dt=dt, output_limits=(-10, 10))
    
    # Initial conditions
    y = 0
    setpoint = 1.0
    
    outputs = []
    
    for t in time:
        # Compute control signal
        u = pid.compute(setpoint, y)
        
        # Simulate system response (Euler method)
        dy = (u - y) / tau
        y += dy * dt
        
        outputs.append(y)
    
    # Plot
    plt.figure(figsize=(10, 6))
    plt.plot(time, outputs, 'b-', label='System Output')
    plt.axhline(y=setpoint, color='r', linestyle='--', label='Setpoint')
    plt.xlabel('Time (s)')
    plt.ylabel('Output')
    plt.title('PID Control Response')
    plt.legend()
    plt.grid(True)
    plt.show()

simulate_pid()

Key Takeaways

  1. āœ… PID combines three control actions
  2. āœ… P responds to current error
  3. āœ… I eliminates steady-state error
  4. āœ… D reduces overshoot
  5. āœ… Anti-windup prevents integral saturation

Next: Chapter 3 - PID Tuning Methods!