TCLab Proportional-only Control

Objective: Quantify the TCLab offset between the setpoint (desired target) and the measured temperature when using a proportional-only controller.

A proportional only (P-only) controller is the simplest implementation of a PID controller without the integral or derivative terms. The controller is an equation that adjusts the controller output, `u(t)`, for input into the system as the manipulated variable. It is a calculation of the difference between the setpoint SP and process variable PV. The adjustable parameter is the controller gain, `K_c`. A large gain produces a controller that reacts aggressively to a difference between the measured PV and target SP.

$$Q(t) = Q_{bias} + K_c \, \left( T_{SP}-T_{PV} \right) = Q_{bias} + K_c \, e(t)$$

The `Q_{bias}` term is a constant that is typically set to the value of `Q(t)` when the controller is first switched from manual to automatic mode. The deviation variable for the heater is the change in value `Q'(t) = Q(t) - Q_{bias}`. For the TCLab, `Q_{bias}`=0 because the TCLab starts with the heater off. The `Q_{bias}` gives bumpless transfer if the error is zero when the controller is turned on. The error from the set point is the difference between the `T_{SP}` and `T_{PV}` and is defined as `e(t) = T_{SP} - T_{PV}`. See additional information on P-only controllers.

The TCLab is a non-integrating process because the temperature returns to ambient conditions when the heater is shut off. A P-only controller has persistent offset between the SP and PV at steady-state for non-integrating systems. The purpose of this assignment is to calculate and then verify the offset for the TCLab.

A common tuning correlation for P-only control is the ITAE (Integral of Time-weighted Absolute Error) method. Use the ITAE setpoint tracking tuning correlation with FOPDT parameters (`K_c`, `\tau_p`, `\theta_p`) determined from the TCLab graphical fitting or TCLab regression exercises.

$$K_c = \frac{0.20}{K_p}\left(\frac{\tau_p}{\theta_p}\right)^{1.22} \quad \mathrm{Set\;point\;tracking}$$

P-Only Offset Simulator

Use the P-Only offset simulator to test the control performance before implementing on the TCLab. The simulator shows the offset for a TCLab FOPDT model with `K_p`=0.9 oC/%, `\tau_p`=175.0 sec, and `\theta_p`=15.0 sec. Use the FOPDT model parameters for your own TCLab device for a more accurate estimate of offset. The controller gain `K_c` is adjusted with a slider to compute the updated offset values as shown on the plot.

import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from scipy.integrate import odeint
import ipywidgets as wg
from IPython.display import display

n = 601 # time points to plot
tf = 600.0 # final time

# TCLab FOPDT
Kp = 0.9
taup = 175.0
thetap = 15.0

def process(y,t,u):
    dydt = (1.0/taup) * (-(y-23.0) + Kp * u)
    return dydt

def pidPlot(Kc):
    t = np.linspace(0,tf,n) # create time vector
    P = np.zeros(n)         # initialize proportional term
    e = np.zeros(n)         # initialize error
    OP = np.zeros(n)        # initialize controller output
    PV = np.ones(n)*23.0    # initialize process variable
    SP = np.ones(n)*23.0    # initialize setpoint
    SP[10:] = 60.0          # step up
    y0 = 23.0               # initial condition
    iae = 0.0
    # loop through all time steps
    for i in range(1,n):
        # simulate process for one time step
        ts = [t[i-1],t[i]]         # time interval
        y = odeint(process,y0,ts,args=(OP[max(0,i-1-int(thetap))],))
        y0 = y[1]                  # record new initial condition
        iae += np.abs(SP[i]-y0[0])
        # calculate new OP with PID
        PV[i] = y[1]               # record PV
        e[i] = SP[i] - PV[i]       # calculate error = SP - PV
        dt = t[i] - t[i-1]         # calculate time step
        P[i] = Kc * e[i]           # calculate proportional term
        OP[i] = P[i]               # calculate new controller output
        if OP[i]>=100:
            OP[i] = 100.0
        if OP[i]<=0:
            OP[i] = 0.0

    # plot PID response
    plt.figure(1,figsize=(15,7))
    plt.subplot(2,2,1)
    plt.plot(t,SP,'k-',linewidth=2,label='Setpoint (SP)')
    plt.plot(t,PV,'r:',linewidth=2,label='Temperature (PV)')
    plt.ylabel(r'T $(^oC)$')
    plt.text(200,30,'Offset: ' + str(np.round(SP[-1]-PV[-1],2)))
    plt.text(400,30,r'$K_c$: ' + str(np.round(Kc,0)))  
    plt.legend(loc='best')
    plt.subplot(2,2,2)
    plt.plot(t,P,'g.-',linewidth=2,label=r'Proportional = $K_c \; e(t)$')
    plt.legend(loc='best')
    plt.subplot(2,2,3)
    plt.plot(t,e,'m--',linewidth=2,label='Error (e=SP-PV)')
    plt.ylabel(r'$\Delta T$ $(^oC)$')
    plt.legend(loc='best')
    plt.xlabel('time (sec)')
    plt.subplot(2,2,4)
    plt.plot(t,OP,'b--',linewidth=2,label='Heater (OP)')
    plt.legend(loc='best')
    plt.xlabel('time (sec)')

Kc_slide = wg.FloatSlider(value=2.0,min=0.0,max=15.0,step=1.0)
wg.interact(pidPlot, Kc=Kc_slide)
print('P-only Simulator: Adjust Kc and Calculate Offset')

P-Only Control with TCLab

Fill in the value of `K_c` and the P-only equation in the code below and run with the TCLab to experimentally determine the offset from a step in setpoint from 23oC to 60oC.

import numpy as np
import matplotlib.pyplot as plt
import tclab
import time

# -----------------------------
# Adjust controller gain (Kc)
#  from ITAE tuning correlation
# -----------------------------
Kc =

n = 600  # Number of second time points (10 min)
tm = np.linspace(0,n-1,n) # Time values
lab = tclab.TCLab()
T1 = np.zeros(n)
Q1 = np.zeros(n)
# step setpoint from 23.0 to 60.0 degC
SP1 = np.ones(n)*23.0
SP1[10:] = 60.0
Q1_bias = 0.0
for i in range(n):
    # record measurement
    T1[i] = lab.T1

    # --------------------------------------------------
    # fill-in P-only controller equation to change Q1[i]
    # --------------------------------------------------
    Q1[i] =

    # implement new heater value
    Q1[i] = max(0,min(100,Q1[i])) # clip to 0-100%
    lab.Q1(Q1[i])
    if i%20==0:
        print(' Heater,   Temp,  Setpoint')
    print(f'{Q1[i]:7.2f},{T1[i]:7.2f},{SP1[i]:7.2f}')
    # wait for 1 sec
    time.sleep(1)
lab.close()
# Save data file
data = np.vstack((tm,Q1,T1,SP1)).T
np.savetxt('P-only.csv',data,delimiter=',',\
           header='Time,Q1,T1,SP1',comments='')

# Create Figure
plt.figure(figsize=(10,7))
ax = plt.subplot(2,1,1)
ax.grid()
plt.plot(tm/60.0,SP1,'k-',label=r'$T_1$ SP')
plt.plot(tm/60.0,T1,'r.',label=r'$T_1$ PV')
plt.ylabel(r'Temp ($^oC$)')
plt.legend(loc=2)
ax = plt.subplot(2,1,2)
ax.grid()
plt.plot(tm/60.0,Q1,'b-',label=r'$Q_1$')
plt.ylabel(r'Heater (%)')
plt.xlabel('Time (min)')
plt.legend(loc=1)
plt.savefig('P-only_Control.png')
plt.show()

Calculate the predicted offset from the FOPDT model. In the solution, include a plot of the P-only controller response. Indicate the measured and calculated offset values graphically on the plot.

Solution

The predicted offset is calculated from the FOPDT model and P-only controller equation.

$$\tau_p \frac{dT'}{dt} = -T' + K_p \, Q'\left(t-\theta_p\right)$$

At steady-state the model becomes:

$$0 = -(T-23) + K_p \, Q'$$

The control output is:

$$Q' = K_c \left(60-T\right)$$

These two equations are combined to solve for `T`:

$$T=\frac{23+K_p\,K_c\,60}{1+K_p\,K_c}$$

and offset=`(60-T)`:

$$\mathrm{offset} = 60-T = 60 - \frac{23+K_p\,K_c\,60}{1+K_p\,K_c}$$

$$\mathrm{offset} = 60-T = 60 - \frac{23+0.9 \times 4.45 \times 60}{1+0.9 \times 4.45}$$

$$\mathrm{offset} = 60-T = 60 - 52.6 = 7.4^oC$$

The plot of the P-only controller response shows a similar offset (`9.1^oC`) to the calculated value (`7.4^oC`).

import numpy as np
import matplotlib.pyplot as plt
import tclab
import time

# -----------------------------
# Adjust controller gain (Kc)
#  from ITAE tuning correlation
# -----------------------------
Kc = 4.45

n = 600  # Number of second time points (10 min)
tm = np.linspace(0,n-1,n) # Time values
lab = tclab.TCLab()
T1 = np.zeros(n)
Q1 = np.zeros(n)
# step setpoint from 23.0 to 60.0 degC
SP1 = np.ones(n)*23.0
SP1[10:] = 60.0
Q1_bias = 0.0
for i in range(n):
    # record measurement
    T1[i] = lab.T1

    # --------------------------------------------------
    # fill-in P-only controller equation to change Q1[i]
    # --------------------------------------------------
    Q1[i] = Q1_bias + Kc * (SP1[i]-T1[i])

    # implement new heater value
    Q1[i] = max(0,min(100,Q1[i])) # clip to 0-100%
    lab.Q1(Q1[i])
    if i%20==0:
        print(' Heater,   Temp,  Setpoint')
    print(f'{Q1[i]:7.2f},{T1[i]:7.2f},{SP1[i]:7.2f}')
    # wait for 1 sec
    time.sleep(1)
lab.close()
# Save data file
data = np.vstack((tm,Q1,T1,SP1)).T
np.savetxt('P-only.csv',data,delimiter=',',\
           header='Time,Q1,T1,SP1',comments='')

# Create Figure
plt.figure(figsize=(10,7))
ax = plt.subplot(2,1,1)
ax.grid()
plt.plot(tm/60.0,SP1,'k-',label=r'$T_1$ SP')
plt.plot(tm/60.0,T1,'r.',label=r'$T_1$ PV')
plt.text(6.1,30,'Measured Offset: ' + str(np.round(SP1[-1]-T1[-1],2)))
offset = 60 - (23+0.9*Kc*60)/(1+0.9*Kc)
plt.text(6.1,26,'Calculated Offset: ' + str(np.round(offset,2)))
plt.text(6.1,22,r'$K_c$: ' + str(np.round(Kc,2)))  
plt.plot([tm[-1]/60.0,tm[-1]/60.0],[SP1[-1],T1[-1]],\
         'b-',lw=3,alpha=0.5)
plt.ylabel(r'Temp ($^oC$)')
plt.xlim([0,10])
plt.legend(loc=2)
ax = plt.subplot(2,1,2)
ax.grid()
plt.plot(tm/60.0,Q1,'b-',label=r'$Q_1$')
plt.ylabel(r'Heater (%)')
plt.xlabel('Time (min)')
plt.legend(loc=1)
plt.savefig('P-only_Control.png')
plt.show()