2.3. PID digital controller: Python implementation#
The aim of this tutorial is to implement temperature control on the TCLab board. The successive stages will be:
definition of the continuous PID corrector from the transfer function identified previously.
determining the maximum sampling time required to maintain a sufficient phase margin.
digitalisation of the corrector in the form of a recurrence equation.
analysis of real performances.
2.3.1. Requirements#
Exercice: Complete the requirements for the temperature control in the following table.
Requirement |
Assessment Criteria |
Level |
|---|---|---|
Control the temperature at 35°C |
Temperature reference tracking |
? |
Settling time at 5 % |
? |
|
Overshoot |
? |
|
Disturbance rejection |
? |
2.3.2. Continuous controller#
2.3.2.1. System transfer functions#
In previous work, we identified the transfer function linking the heating control (input) \(Q_1\) to the temperature (output) \(T_1\) as second order:
\(G_1(p) = \frac{\Delta T_1(p)}{Q_1(p)}= \frac{0.65}{(27p + 1)(160p + 1)} \)
with \(\Delta T_1\) expressing the relative difference between the sensor temperature and the ambient temperature (around 20°C).
Question: Approximate the transfer function \(G_1(p)\) as a 1st order with pure delay.
2.3.2.2. Zielgler-Nichols method#
The Ziegler-Nichols approach to setting PID controllers can be used as an initial parameterisation. This approach is based on a transfer function of the form: \( G(s) = \frac{Ke^{-Ls}}{\tau s + 1} \) with \(R=K/\tau\) with
\(K\) : the steady-state gain;
\(\tau\) : the dominant time-constant;
\(L\) : the pure time-delay.
The form (P, PI, PD or PID) of the PID controller can then be chosen according to the value of \(\tau\) compared to \(L\) by using following table:
\(\frac{Time Constant}{Delay}=\frac{\tau}{L}\) |
Best controller |
|---|---|
\(\frac{\tau}{L} > 20 \) |
On-Off controller |
\(10 < \frac{\tau}{L} < 20 \) |
P controller |
\(5 < \frac{\tau}{L} < 10 \) |
PI controller |
\(2 < \frac{\tau}{L} < 5 \) |
PID controller |
Then the following table defines the parameters of a P, PI or PID controller: \(G_c(s)=k_p(1+\frac{1}{\tau_i s}+\tau_d s)=k_p+\frac{k_i}{s}+k_d s\)
Type |
\(k_p\) |
\(k_i\) |
\(k_d\) |
|---|---|---|---|
P |
\(\frac{1}{RL}\) |
||
PI |
\(\frac{0.9}{RL}\) |
\(\frac{3}{10RL^2}\) |
|
PID |
\(\frac{1.2}{RL}\) |
\(\frac{0.6}{RL^2}\) |
\(\frac{0.6}{R}\) |
Exercice: Select and calculate the corrector.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
# Ziegler Nichols input parameter
tau = 160 # [s] time constant of the 1st order
L= 27 # [s] delay
K = 0.65 # [°/%] static gain
R=K/tau
# Controller type
# ??
# controller coefficient with Ziegler Nichols approach
# ??
#print("PID coefficient (parrallel form): ")
#print("Kp = %.3f"%Kp)
#print("Ki = %.3f"%Ki)
#print("Kd = %.3f"%Kd)
2.3.2.3. Temporal performances#
Exercice: Analyse the performance by simulation in terms of time: the following code should plot the time response \(T_1\) to a 1° step order. It is also interesting to plot the power demand \(Q_1\). You can use
tf,stepfromcontrol.matlabpackage.
import control.matlab as control
# system transfer function, based on second order model
G1 = 0.65*control.tf([1],[27,1])*control.tf([1],[160,1])
# controller and global model transfer functions
# ??
# Step response of the closed loop
t = np.linspace(0,400,1000)
# ??
#plt.plot(t,y, 'b', t,np.ones(1000),'g-')
#plt.xlabel('Time (s)')
#plt.ylabel('Output (t°C)')
#plt.show()
#plt.plot(t,Q, 'r')
#plt.xlabel('Time (s)')
#plt.ylabel('Power (%) input')
#plt.grid()
#plt.show()
2.3.2.4. Frequency performances#
By plotting the Bode and Nichols diagrams in open or closed loops, we can establish the stability margins and the bandwidth.
Exercice: Analyse the frequency performances with Bode plots in open loop and closed loop. Adjust PID gains if necessary. Nichols diagram can be also usefull. You can use
bode(withmargins=Trueparameter),nicholsfromcontrol.matlabpackage.
# Bode diagram of open loops
# Print phase and gain margins
# Print Nichols diagram
2.3.2.5. Optimal design of the controller#
Exercice: Using the observations made, define an optimum design for the controller. This can be done using an optimisation algorithm such as
scipy.optimize.fmin_slsqp(func, x0, bounds, f_ieqcons)from thescipypackage where
funcis a function which return the objective to be minimised
f_ieqconsis a function which return one or more constraints that must be positive in order to be respected.
x0is the initial set of parameter (vector) for objective anc ocnstraints functions.
import scipy
# Problem definition
def optim_model(x, param):
Kp=x[0]
Ki=x[1]
Kd=0
# controller and global model transfer functions
# ??
#if (param=='constraints'):
# return [??]
#elif (param=='objective'):
# return ??
# Initial point
# InitPoint=np.array([??, ??])
# bounds=[(min ?, max ?),
# (min ?, max ?)]
# Resolution of the problem
contrainte = lambda x: optim_model(x, "constraints")
objectif = lambda x: optim_model(x, "objective")
# result = scipy.optimize.fmin_slsqp(func=objectif,x0=InitPoint, bounds=bounds, f_ieqcons=contrainte)
2.3.3. Digital controller#
2.3.3.1. Definition of minimum sampling time#
Exercise: Calculate the minimum sampling time required to avoid degrading the phase margin by more than 5°. Check your result using Padé’s approximation.
# Phase = -delay.w = 5° where delay=Ts/2
#gm, pm, wcg, wcp = control.margin(T)
# T_s = ??
T_s = 1 # To modify ?
print("Maximum sampling time: %.2e s"%T_s)
delay=T_s/2
num,den=control.pade(delay, 3)
pade=control.tf(num,den)
mag, phase, omega= control.bode(pade, plot=False, omega_limits=[1,2*3.14*.1], Hz=False)
import matplotlib.pyplot as plt
import numpy as np
freq=omega/2/np.pi
plt.tight_layout()
fig, (ax1, ax2) = plt.subplots(2) # get subplot axes
plt.sca(ax1) # magnitude plot
plt.plot(freq, [1]*len(omega),'r--', label='Exact')
plt.plot(freq, mag,'b--', label='Pade')
plt.ylabel('Gain')
plt.legend()
plt.sca(ax2) # phase plot
plt.plot(freq,-delay*omega*180/np.pi,'r--', label='Exact')
plt.plot(freq, phase*180/np.pi,'b--', label='Pade')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Phase (°)')
plt.legend()
Maximum sampling time: 1.00e+00 s
<matplotlib.legend.Legend at 0x7f2638778a30>
<Figure size 640x480 with 0 Axes>
#PIDSysPade=T*pade
#mag, phase, omega= control.bode(PIDSysPade, plot=True, Hz=True, initial_phase=0, margins=True)
2.3.4. Digital PID controller#
2.3.4.1. Recurrence equation#
Question: - Discretize the PID controller in the form of a recurrence equation.
Exercice: Define a PID function which returns
\(o_k = o_{k-1} + K_i e_k T_s + K_p (e_k-e_{k-1}) + \frac{K_d}{T_s}(e_k+ e_{k-2}-2e_{k-1})\)
where \(e_k\) is the error and \(o_k\) the output of the controller. Theglobalkeyword defines variables which belongs to the global scope.
def PID(order, measure):
#global ??
return max(0, min(100, o_k))
2.3.4.2. Real tests#
The following cells can be used to test your controller. The first step is to check that your card is responding correctly.
%matplotlib inline
from tclab import TCLab
# if no card you can use the simulator:
# from tclab import TCLabModel as TCLab
from tclab import clock # from tclab import clock
# Start TCLab
# lab = TCLab()
tperiod = 20
tstep = 1
#lab.Q1(100)
#for t in clock(tperiod,tstep):
# print("Time %4.1f sec. : Temp = %.2f"%(t,lab.T1))
#lab.Q1(0)
We will use a specific window to plot corrector results:
!pip install PySide6
!pip install PyQt6
import PyQt6
import PySide6
The following cell provides an initial implementation of PID control for heater T1.
print("Temperature 1: %0.2f °C"%(lab.T1))
print("Temperature 2: %0.2f °C"%(lab.T2))
import matplotlib.pyplot as plt
import numpy as np
time = []
Q1 = []
Kperror = []
T1 = []
Torder =35
tfinal = 400
tstep = T_s
%matplotlib qt
# Interactive mode
plt.ion() # ion => interactive on / ioff => interactive off
plt.figure()
plt.plot(time, T1,'b-', label='T1')
plt.plot(time, Q1,'r-', label='Q1')
plt.plot(time, np.ones(len(time))*Torder,'g-', label='Order')
plt.xlabel('Time (s)')
plt.ylabel('Temperature (°C) - Heating power /2 (%)')
plt.legend()
plt.show(block=False)
for t in clock(tfinal, tstep):
Tmes=lab.T1
Q = PID(Torder, Tmes)
lab.Q1(Q)
time = time + [t]
T1 = T1 + [Tmes]
Q1 = Q1 + [Q]
Title='T1 = %.1f °C - Q1 = %.1f'%(Tmes,Q)
plt.title(Title)
plt.plot(time, T1,'b-')
plt.plot(time, Q1,'y-')
plt.plot(time, np.ones(len(time))*Torder,'g-')
#plt.show(block=False)
plt.pause(0.05)
plt.ioff()
print("\nTurn Heater Q1 Off")
lab.Q1(0)
#lab.close()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[13], line 1
----> 1 print("Temperature 1: %0.2f °C"%(lab.T1))
2 print("Temperature 2: %0.2f °C"%(lab.T2))
4 import matplotlib.pyplot as plt
NameError: name 'lab' is not defined
print("\nTurn Heater Q1 Off")
#lab.Q1(0)
Turn Heater Q1 Off
Finally, you can display the temperature and power curves:
%matplotlib inline
plt.ioff()
plt.plot(time, T1,'b-', label='T1')
plt.plot(time, np.ones(len(time))*Torder,'g-', label='Order')
plt.xlabel('Time (s)')
plt.ylabel('Temperature (°C)')
plt.legend()
plt.show()
plt.plot(time, Q1,'r-', label='Q1')
plt.xlabel('Time (s)')
plt.ylabel('Puissance de chauffe (%)')
plt.legend()
plt.show()