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.
%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 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 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()