Skip to content

PID Controller

The PID class provides a standard Proportional-Integral-Derivative (PID) controller implementation for process control applications. PID controllers are widely used in industrial control systems to maintain a process variable (like temperature, pressure, or flow rate) at a desired setpoint.

Import

from pydoover.utils import PID

Class Reference

PID

A PID controller that calculates control output based on the error between a setpoint and the current process value.

Constructor

PID(
    Kp: float,
    Ki: float,
    Kd: float,
    setpoint: float = 0,
    output_limits: tuple = (None, None)
)

Parameters

ParameterTypeDefaultDescription
KpfloatrequiredProportional gain
KifloatrequiredIntegral gain
KdfloatrequiredDerivative gain
setpointfloat0Target value the controller tries to achieve
output_limitstuple(None, None)Min/max limits for output as (min, max)

Methods

update()

Update the PID controller with the current feedback value and get the control output.

def update(self, feedback_value: float, dt: float = None) -> float
ParameterTypeDefaultDescription
feedback_valuefloatrequiredCurrent value from the process
dtfloatNoneTime interval; calculated automatically if not provided

Returns: The control output value.

set_setpoint()

Set a new target value for the PID to reach.

def set_setpoint(self, setpoint: float) -> None

set_output_limits()

Set the minimum and maximum output limits.

def set_output_limits(self, min_output: float, max_output: float) -> None

set_last_output()

Set the last output value (useful for bumpless transfer).

def set_last_output(self, output: float) -> None

set_last_error()

Set the last error value.

def set_last_error(self, error: float) -> None

set_integral_output()

Initialize the integral term to achieve a desired output value.

def set_integral_output(self, integral_output: float) -> None

reset()

Reset the internal state of the PID controller.

def reset() -> None

PID Control Theory

A PID controller continuously calculates an error value as the difference between a desired setpoint and a measured process variable. It applies a correction based on three terms:

Proportional Term (P)

The proportional term produces an output proportional to the current error:

P = Kp * error
  • Higher Kp values result in faster response but may cause overshoot
  • Lower Kp values result in slower response but more stability

Integral Term (I)

The integral term accumulates error over time to eliminate steady-state error:

I = Ki * integral(error * dt)
  • The integral term helps eliminate persistent offset from the setpoint
  • Too high Ki can cause overshoot and oscillation

Derivative Term (D)

The derivative term predicts future error based on the rate of change:

D = Kd * d(error)/dt
  • The derivative term dampens the system response
  • Helps reduce overshoot and improve stability

Combined Output

output = P + I + D

Examples

Basic Temperature Control

from pydoover.utils import PID
import time

# Create a PID controller for temperature control
# Setpoint: 25 degrees, output limited to 0-100% heater power
pid = PID(
    Kp=2.0,      # Proportional gain
    Ki=0.1,      # Integral gain
    Kd=0.05,     # Derivative gain
    setpoint=25.0,
    output_limits=(0, 100)
)

def control_loop():
    while True:
        # Read current temperature from sensor
        current_temp = read_temperature_sensor()

        # Calculate control output
        heater_power = pid.update(current_temp)

        # Apply control output
        set_heater_power(heater_power)

        print(f"Temp: {current_temp:.1f}C, Heater: {heater_power:.1f}%")

        time.sleep(1)

Motor Speed Control

from pydoover.utils import PID

# PID for motor speed control
# Target: 1000 RPM, output is PWM duty cycle (0-255)
motor_pid = PID(
    Kp=0.5,
    Ki=0.2,
    Kd=0.1,
    setpoint=1000,
    output_limits=(0, 255)
)

def motor_control_loop():
    while True:
        current_rpm = read_encoder_rpm()
        pwm_value = motor_pid.update(current_rpm)
        set_motor_pwm(int(pwm_value))
        time.sleep(0.01)  # 100 Hz control loop

Changing Setpoint Dynamically

from pydoover.utils import PID

pid = PID(Kp=1.0, Ki=0.1, Kd=0.05, setpoint=50)

# Initial control at setpoint 50
for _ in range(100):
    value = read_sensor()
    output = pid.update(value)
    apply_output(output)

# Change setpoint to 75
pid.set_setpoint(75)

# Continue control at new setpoint
for _ in range(100):
    value = read_sensor()
    output = pid.update(value)
    apply_output(output)

Manual Time Delta

For deterministic control loops or testing, you can provide the time delta manually:

from pydoover.utils import PID

pid = PID(Kp=1.0, Ki=0.1, Kd=0.05, setpoint=100)

# Fixed 10ms control interval
dt = 0.01

while True:
    value = read_sensor()
    output = pid.update(value, dt=dt)
    apply_output(output)
    time.sleep(dt)

Bumpless Transfer

When switching from manual to automatic control, use set_last_output() and set_integral_output() for smooth transition:

from pydoover.utils import PID

pid = PID(Kp=1.0, Ki=0.1, Kd=0.05, setpoint=50)

# System was running manually at 30% output
manual_output = 30.0
current_value = 45.0

# Initialize PID for bumpless transfer
pid.set_last_output(manual_output)
pid.set_integral_output(manual_output)
pid.set_last_error(pid.setpoint - current_value)

# Now switch to automatic control - no sudden jump in output
while True:
    value = read_sensor()
    output = pid.update(value)
    apply_output(output)

Integration with pydoover Application

from pydoover.docker import Application
from pydoover.utils import PID

class TemperatureController(Application):
    def setup(self):
        # Initialize PID controller
        self.pid = PID(
            Kp=2.0,
            Ki=0.1,
            Kd=0.05,
            setpoint=25.0,
            output_limits=(0, 100)
        )

        # Get platform interface for I/O
        self.platform = self.get_platform_interface()

    async def main_loop(self):
        while self.running:
            # Read temperature from analog input
            temp = self.platform.get_analog_input(0)

            # Calculate control output
            power = self.pid.update(temp)

            # Set heater power via analog output
            self.platform.set_analog_output(0, power)

            # Update UI
            self.ui_manager.update_variable("temperature", temp)
            self.ui_manager.update_variable("heater_power", power)

            await asyncio.sleep(1)

    def handle_setpoint_change(self, new_setpoint):
        """Called when user changes setpoint via UI"""
        self.pid.set_setpoint(new_setpoint)

Tuning Guidelines

Ziegler-Nichols Method

A common method for tuning PID controllers:

  1. Set Ki and Kd to 0
  2. Increase Kp until the system oscillates with constant amplitude (ultimate gain Ku)
  3. Measure the oscillation period Tu
  4. Calculate gains:
    • Kp = 0.6 * Ku
    • Ki = 2 * Kp / Tu
    • Kd = Kp * Tu / 8

General Guidelines

SymptomAdjustment
Slow responseIncrease Kp
OvershootDecrease Kp or increase Kd
OscillationDecrease Kp and/or Ki
Steady-state errorIncrease Ki
Noisy outputDecrease Kd

Starting Values

For a typical process control application:

# Conservative starting point
pid = PID(Kp=1.0, Ki=0.1, Kd=0.05, setpoint=target)

# Aggressive (faster response)
pid = PID(Kp=5.0, Ki=0.5, Kd=0.2, setpoint=target)

Internal State

The PID controller maintains internal state between calls:

AttributeDescription
_last_timeTimestamp of last update
_last_errorPrevious error value (for derivative)
_integralAccumulated integral term
_last_outputPrevious output value

Call reset() to clear all internal state when starting fresh.

Related Pages