Skip to content

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
)
ParameterTypeDefaultDescription
Kpfloat--Proportional gain. Responds to the current error.
Kifloat--Integral gain. Responds to accumulated error over time.
Kdfloat--Derivative gain. Responds to the rate of error change.
setpointfloat0The target value the controller tries to achieve.
output_limitstuple(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)
ParameterTypeDefaultDescription
feedback_valuefloat--The current measured process value.
dtfloatNoneTime 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.

Information Circle

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

MethodDescription
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