PID Control with Anti-Reset-Windup

Additional Benefits of Tracking the Manipulated Variable

An extra tracking input was added in order to improve response when switching from manual to automatic control. This turns out to have additional benefits for the case where the controller would otherwise be requesting infeasible values for the manipulated variable.

The next simulation simply repeats the applies the tracking controller to the heater startup case.

In [1]:
def PID(Kp, Ki, Kd, MV_bar=0, beta=1, gamma=0):
    # initialize stored data
    eD_prev = 0
    t_prev = -100
    P = 0
    I = 0
    D = 0
    
    # initial control
    MV = MV_bar
    
    while True:
        # yield MV, wait for new t, SP, PV, TR
        t, PV, SP, TR = yield MV
        
        # adjust I term so output matches tracking input
        I = TR - MV_bar - P - D
        
        # PID calculations
        P = Kp*(beta*SP - PV)
        I = I + Ki*(SP - PV)*(t - t_prev)
        eD = gamma*SP - PV
        D = Kd*(eD - eD_prev)/(t - t_prev)
        MV = MV_bar + P + I + D
        
        # update stored data for next iteration
        eD_prev = eD
        t_prev = t
In [2]:
%matplotlib inline
from tclab import clock, setup, Historian, Plotter

TCLab = setup(connected=False, speedup=10)

controller = PID(2, 0.1, 2, beta=0)   # create pid control
controller.send(None)                 # initialize

tfinal = 800

with TCLab() as lab:
    h = Historian([('SP', lambda: SP), ('T1', lambda: lab.T1), ('MV', lambda: MV), ('Q1', lab.Q1)])
    p = Plotter(h, tfinal)
    T1 = lab.T1
    for t in clock(tfinal, 2):
        SP = T1 if t < 50 else 50                     # get setpoint
        PV = lab.T1                                   # get measurement
        MV = controller.send([t, PV, SP, lab.Q1()])   # compute manipulated variable
        lab.Q1(MV)                                    # apply 
        p.update(t)                                   # update information display
TCLab Model disconnected successfully.

We observe markedly improved performance with less overshoot of the setpoint, less undershoot, and faster settling time.

The reason for the improved response is that the integral term of the PID controller is constrained such that the manipulated variable remains within feasible limits. This important feature is called anti-reset windup.

Embedding Anti-Reset Windup inside the Controller

The most common reason for a mismatch between the controller output and the actual value of the manipulated variable are the existence of hard limits on $MV$. These hard limits can be enforced inside of the control algorithm which may, for some applications, eliminate the need for using the tracking input.

The next cell modifies our control algorithm to accomodate this feature, and automatically detects if a tracking signal is passed to the controller.

In [1]:
def PID(Kp, Ki, Kd, MV_bar=0, beta=1, gamma=0):
    # initialize stored data
    eD_prev = 0
    t_prev = -100
    P = 0
    I = 0
    D = 0
    
    # initial control
    MV = MV_bar
    
    while True:
        # yield MV, wait for new t, SP, PV, TR
        data = yield MV
        
        # see if a tracking data is being supplied
        if len(data) < 4:
            t, PV, SP = data
        else:
            t, PV, SP, TR = data
            I = TR - MV_bar - P - D
        
        # PID calculations
        P = Kp*(beta*SP - PV)
        I = I + Ki*(SP - PV)*(t - t_prev)
        eD = gamma*SP - PV
        D = Kd*(eD - eD_prev)/(t - t_prev)
        MV = MV_bar + P + I + D
        
        # Constrain MV to range 0 to 100 for anti-reset windup
        MV = 0 if MV < 0 else 100 if MV > 100 else MV
        #I = MV - MV_bar - P - D
        
        # update stored data for next iteration
        eD_prev = eD
        t_prev = t

This version of the control is tested again with our startup example, but this time without using the tracking input to the controller. The results demonstrate the importance of anti-reset windup.

In [2]:
550 % 100
Out[2]:
50
In [5]:
%matplotlib inline
from tclab import clock, setup, Historian, Plotter

TCLab = setup(connected=False, speedup=10)

controller = PID(1, 0.2, 0, beta=0)   # create pid control
controller.send(None)                 # initialize

tfinal = 1200

with TCLab() as lab:
    h = Historian([('SP', lambda: SP), ('T1', lambda: lab.T1), ('MV', lambda: MV), ('Q1', lab.Q1)])
    p = Plotter(h, tfinal)
    Tlo = lab.T1
    Thi = 120
    for t in clock(tfinal, 2):
        SP =  Thi if (t % 600 < 100) else Tlo                    # get setpoint
        PV = lab.T1                                   # get measurement
        MV = controller.send([t, PV, SP])   # compute manipulated variable
        lab.Q1(MV)                                    # apply 
        p.update(t)                                   # update information display
TCLab Model disconnected successfully.
In [9]:
h.columns
Out[9]:
['Time', 'SP', 'T1', 'MV', 'Q1']