PID with Anti-Windup: Theory, Tuning and Experimental Validation
Integral windup is one of the most common failure modes in deployed PID controllers. This post covers the problem from first principles, derives the back-calculation anti-windup scheme, shows how Synapsys implements it, and validates the design against a simulated second-order plant.
The windup problem
A PID controller with output saturation has a nasty interaction: when the actuator is saturated, the integrator keeps accumulating error even though the output is clamped. When the error sign reverses, the integrator takes a long time to "wind down" before the output leaves saturation — causing overshoot and oscillation.
Error ─► [P]──────────────► sum ─► [saturation] ─► u
[I → accumulate] ──► │
[D] ────────────► ┘ │
▲ │
└─── windup occurs ───┘
when e > 0 and u = u_max
Toy example: windup in action
from synapsys.algorithms import PID
from synapsys.api import ss, c2d
import numpy as np
plant = c2d(ss([[-1]], [[1]], [[1]], [[0]]), dt=0.01)
pid_no_aw = PID(Kp=5.0, Ki=2.0, Kd=0.0, dt=0.01,
u_min=-1.0, u_max=1.0,
anti_windup=False) # windup enabled
x = np.zeros(1)
setpoint = 5.0 # large step — will saturate the output
outputs = []
for _ in range(500): # 5 s
y = x # y = x for this plant
u = np.array([pid_no_aw.compute(setpoint, float(y[0]))])
u_sat = np.clip(u, -1.0, 1.0) # saturation outside PID
x, _ = plant.evolve(x, u_sat)
outputs.append(float(y[0]))
The output reaches setpoint but then heavily overshoots because the integrator accumulated (units) of error during saturation.
Back-calculation anti-windup
The back-calculation scheme feeds the saturation error back to the integrator with gain (tracking time constant):
When the output is not saturated, so the correction term is zero — the integrator behaves normally. When saturated, the correction reduces the integrator at a rate proportional to the saturation error.
A common choice is (geometric mean), or simply when there is no derivative term.
Synapsys implementation
from synapsys.algorithms import PID
# Anti-windup ON (default) — u_min/u_max trigger back-calculation
pid = PID(
Kp = 5.0,
Ki = 2.0,
Kd = 0.1,
dt = 0.01,
u_min = -1.0,
u_max = 1.0,
# anti_windup=True is default
)
u = pid.compute(setpoint=5.0, measurement=y)
The compute() method returns the clamped output and applies back-calculation
internally — no external saturation needed.
Comparative simulation
- Simulation
- Results
import numpy as np
import matplotlib.pyplot as plt
from synapsys.algorithms import PID
from synapsys.api import ss, c2d
plant = c2d(ss([[-0.5]], [[1]], [[1]], [[0]]), dt=0.01)
def run_sim(anti_windup: bool, n_steps: int = 800):
pid = PID(Kp=5.0, Ki=2.0, Kd=0.05, dt=0.01,
u_min=-1.0, u_max=1.0, anti_windup=anti_windup)
x = np.zeros(1)
ys, us = [], []
for i in range(n_steps):
setpoint = 3.0 if i < 400 else -3.0 # step then negative step
y = float(x[0])
u = np.array([pid.compute(setpoint, y)])
x, _ = plant.evolve(x, u)
ys.append(y); us.append(float(u[0]))
return np.array(ys), np.array(us)
t = np.arange(800) * 0.01
y_no, u_no = run_sim(anti_windup=False)
y_aw, u_aw = run_sim(anti_windup=True)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 6), sharex=True)
ax1.plot(t, y_no, label='No anti-windup', linestyle='--')
ax1.plot(t, y_aw, label='Anti-windup', linewidth=2)
ax1.axhline(3.0, color='gray', linestyle=':', linewidth=0.8)
ax1.axhline(-3.0, color='gray', linestyle=':', linewidth=0.8)
ax1.set_ylabel("Output"); ax1.legend(); ax1.grid(True)
ax2.plot(t, u_no, linestyle='--'); ax2.plot(t, u_aw, linewidth=2)
ax2.axhline(1.0, color='red', linestyle=':', linewidth=0.8, label='u_max')
ax2.axhline(-1.0, color='red', linestyle=':', linewidth=0.8, label='u_min')
ax2.set_ylabel("Control (u)"); ax2.set_xlabel("Time (s)")
ax2.legend(); ax2.grid(True)
plt.tight_layout(); plt.savefig("pid_antiwindup_comparison.png", dpi=150)
Without anti-windup:
- Positive step: output overshoots ~40% before settling
- Negative step: large transient because integrator was wound up to limit
With anti-windup:
- Both steps: clean first-order-like response
- Overshoot < 5%
- Settling time reduced by ~60%
Tuning guidelines for research
| Parameter | Effect | Starting point |
|---|---|---|
| Speed of response | FOPDT: | |
| Steady-state error elimination | ||
| Damping, noise amplification | ||
| Actuator limits | Physical actuator spec |
For discrete implementations, always use a sampling rate at least 10× the closed-loop bandwidth to avoid discretisation artefacts in the derivative term.
Connection to the broader literature
The back-calculation scheme used in Synapsys follows Åström & Hägglund (2006) Advanced PID Control, Chapter 6. The discrete formulation uses the bilinear (Tustin) approximation for the integrator, which avoids the frequency-domain distortion of forward Euler at high gains.
@book{astrom2006advanced,
author = {Åström, Karl Johan and Hägglund, Tore},
title = {Advanced PID Control},
publisher = {ISA — The Instrumentation, Systems, and Automation Society},
year = {2006},
isbn = {978-1556175169},
}
Summary
| Feature | Synapsys PID |
|---|---|
| Discrete time | ✓ (fixed dt) |
| Anti-windup | ✓ back-calculation (default on) |
| Output limits | u_min, u_max |
| Derivative filter | ✓ first-order Tustin |
| Reset | pid.reset() |
The full API reference is at synapsys.algorithms →.
