PID Controller
The PID class implements a standard proportional-integral-derivative controller for feedback control loops. PID controllers are used to automatically adjust an output (such as pump speed, valve position, or heater power) to drive a measured process variable towards a desired setpoint.
Constructor
from pydoover.utils import PID
pid = PID(
Kp=1.0, # Proportional gain
Ki=0.1, # Integral gain
Kd=0.05, # Derivative gain
setpoint=50.0, # Target value
output_limits=(0, 100), # (min, max) output bounds
)
| Parameter | Type | Default | Description |
|---|---|---|---|
Kp | float | -- | Proportional gain. Responds to the current error. |
Ki | float | -- | Integral gain. Responds to accumulated error over time. |
Kd | float | -- | Derivative gain. Responds to the rate of error change. |
setpoint | float | 0 | The target value the controller tries to achieve. |
output_limits | tuple | (None, None) | Min and max bounds for the output. |
Understanding the Gains
Each gain term addresses a different aspect of the control problem:
- Kp (Proportional) -- produces an output proportional to the current error. Higher values react more aggressively but can cause overshoot and oscillation. Start here when tuning.
- Ki (Integral) -- accumulates error over time to eliminate steady-state offset. Without it, the controller may settle at a value that is close to but not exactly at the setpoint. Too high and it causes slow oscillation.
- Kd (Derivative) -- dampens the response by reacting to how fast the error is changing. Helps reduce overshoot from P and I terms, but amplifies noise in the feedback signal.
Updating the Controller
Call update() each iteration of your control loop with the current measured value. It returns the control output:
output = pid.update(feedback_value, dt=None)
| Parameter | Type | Default | Description |
|---|---|---|---|
feedback_value | float | -- | The current measured process value. |
dt | float | None | Time interval in seconds since the last update. If not provided, the controller calculates it automatically using wall-clock time. |
On the first call, update() initialises internal state and returns the last output value (defaulting to 0). Subsequent calls compute the full PID output.
If your control loop runs at irregular intervals, pass dt explicitly to avoid timing inaccuracies. If your loop runs at a consistent rate, you can let the controller calculate dt internally.
Output Limits
Output limits prevent the controller from commanding values outside a safe range. Set them at construction time or update them later:
pid.set_output_limits(min_output=0, max_output=100)
When limits are set, the output is clamped to the specified range. This does not affect the integral accumulator directly -- see the next section for integral windup handling.
Integral Windup Handling
When the output is saturated at its limits for an extended period, the integral term can accumulate a large value (windup). This causes sluggish response when the error eventually changes direction. Use set_integral_output() to reset the integral accumulator to a value that corresponds to a desired output:
# Reset the integral so the controller's integral contribution equals 30 pid.set_integral_output(30)
This sets _integral = integral_output / Ki, so the integral term will produce the specified output. This is useful when transitioning between manual and automatic control modes, or when recovering from a fault condition.
Changing the Setpoint
Update the target value at any time:
pid.set_setpoint(60.0)
Resetting the Controller
Reset all internal state to start fresh:
pid.reset()
This clears the integral accumulator, last error, last output, and timing state. Use this when switching between operating modes or after a process restart.
Additional Methods
| Method | Description |
|---|---|
set_last_output(output) | Set the stored last output value. Useful when resuming control from a known state. |
set_last_error(error) | Set the stored last error value. |
Practical Example
This example controls a pump speed to maintain a target pressure of 250 kPa. The PID controller adjusts the pump speed percentage based on pressure feedback:
from pydoover.utils import PID
# Create a PID controller targeting 250 kPa
# Output is pump speed as a percentage (0-100%)
pid = PID(
Kp=2.0,
Ki=0.5,
Kd=0.1,
setpoint=250.0,
output_limits=(0, 100),
)
async def main_loop(self):
# Read the current pressure from a sensor
current_pressure = await self.read_pressure_sensor()
# Calculate the new pump speed
pump_speed = pid.update(current_pressure)
# Apply the output to the pump
await self.set_pump_speed(pump_speed)
If the pressure reads 200 kPa (below the 250 kPa setpoint), the controller increases pump speed. As the pressure approaches the setpoint, the output stabilises. If pressure overshoots, the controller reduces pump speed.
Related Pages
- Kalman Filter -- smooth noisy sensor readings before feeding them to the PID controller
- Alarms -- trigger alerts when the process variable exceeds safe limits
- Utilities Overview -- all available utility modules