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
| Parameter | Type | Default | Description |
|---|---|---|---|
Kp | float | required | Proportional gain |
Ki | float | required | Integral gain |
Kd | float | required | Derivative gain |
setpoint | float | 0 | Target value the controller tries to achieve |
output_limits | tuple | (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
| Parameter | Type | Default | Description |
|---|---|---|---|
feedback_value | float | required | Current value from the process |
dt | float | None | Time 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
Kpvalues result in faster response but may cause overshoot - Lower
Kpvalues 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
Kican 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:
- Set
KiandKdto 0 - Increase
Kpuntil the system oscillates with constant amplitude (ultimate gainKu) - Measure the oscillation period
Tu - Calculate gains:
Kp = 0.6 * KuKi = 2 * Kp / TuKd = Kp * Tu / 8
General Guidelines
| Symptom | Adjustment |
|---|---|
| Slow response | Increase Kp |
| Overshoot | Decrease Kp or increase Kd |
| Oscillation | Decrease Kp and/or Ki |
| Steady-state error | Increase Ki |
| Noisy output | Decrease 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:
| Attribute | Description |
|---|---|
_last_time | Timestamp of last update |
_last_error | Previous error value (for derivative) |
_integral | Accumulated integral term |
_last_output | Previous output value |
Call reset() to clear all internal state when starting fresh.
Related Pages
- Utilities Overview - Overview of all utility functions
- Kalman Filter - Filter noisy sensor data before PID
- Platform Interface - Reading sensors and controlling outputs