When I was in the university, my control system classes was dictated using MATLAB, MATLAB is a powerfull tool to design and test control systems. But, one of the things that overwhelms me about Matlab, is that it is a very heavy program with different types of packages for different uses and a large number of tools and I like a simple minimalist coding style stuff for create or design controllers.
As I am familiar with Python, I became curious to utilize this language to design a basic control system such as a PID controller, applying the theory I learned in university.
So here is what I learned.
What is a PID controller
A PID controller is a "brain" for machines that helps them stay on track. It takes in sensory input, compares it to a target, and adjusts as needed to achieve the desired outcome.
A basic diagram of a PID controller looks like this:
e(t) is known as the error signal, which is the difference between a desired process value or setpoint r(t) and a measured process variable y(t) from the system that we want to control called "plant". u(t) is the output of the PID controller known as the control signal that will attempts to minimize the error e(t) over time.
The PID controller is made up of three distinct parts: the proportional component, the integral component, and the derivative component.
The proportional part helps to maintain the desired output of the control system by working to reduce the difference between the desired output and the actual output of the system. For example, the cruise control that keeps you at the right speed when you're driving on a flat road. If you start going too slow, the proportional part tells the car to go a little faster so you get back to the right speed. If you start going too fast, it tells the car to go slower.
The integral part, keeps track of how much the system has been off from the desired output in the past and makes adjustments to help reduce that difference over time. It's like a person who remembers that they were too hot or too cold in a room yesterday and adjusts the thermostat today to make sure they're more comfortable.
The derivative part of a PID controller is like a watchdog that watches how things are changing over time. It looks for how fast things are changing and makes adjustments based on that. Imagine you're going downhill in your car and you start going faster, the derivative part of a cruise control system would sense this change in speed and make adjustments to slow you down.
Mathematically, in time domain the PID controller looks like this:
Where:
-
: represents how much the output of the controller changes in response to a difference between the desired output and the actual output of the system.
-
: represents how much the output of the controller changes in response to the accumulated difference between the desired and actual outputs of the system over time.
-
: represents how much the output of the controller changes in response to the rate of change of the difference between the desired and actual outputs of the system.
-
: represents the error signal.
-
: represents control signal.
Desiging the PID controller
We will be using example 8-1 (found on page 572) from the book 'Modern Control Engineering' by Katushiko Ogata to create our PID controller. Additionally, we will utilize the python-control library, as well as sympy and numpy for mathematical operations, and Matplotlib for visualizing the controller's responses.
Creating the Plant model
We need a system plant to control before creating our PID controller. To do this, we can utilize the python-control package.
You might be curious as to why 's' is used instead of time in the Laplace domain. The reason is that the Laplace domain enables analysis of transfer functions in a simpler and more efficient manner for linear time-invariant systems (LTI)
So we create our model transfer function system like this.
import control
s = control.TransferFunction.s
plant = 1/(s*(s+1)*(s+5))
Creating the PID model
Let's create our PID in Laplace domain.
Where:
-
: represents how much the output of the controller changes in response to a difference between the desired output and the actual output of the system.
-
: represents the integral time.
-
: represents the derivative time.
Our goal now is to adjust the values of , , and for optimal performance. We will utilize the Ziegler-Nichols tuning rule to determine these values.
Since our plant contains an integrator (), we will utilize the second method of the Ziegler-Nichols tuning rule. This involves setting cancelling it's effect and in order to obtain the closed-loop transfer function.
Having obtained the transfer function of our plant using python-control, we can employ the feedback method to determine the closed-loop transfer function. To calculate this, we will add an arbitrary value to , set to 0, and assign a large value to .
import control
s = control.TransferFunction.s
plant = 1/(s*(s+1)*(s+5))
Kp = 17
Ti = 10000000000
Td = 0
pid = Kp*(1 + 1/Ti*s + Td*s)
feedback = control.feedback(plant*pid)
print(feedback)
#result
# 1.7e-08 s + 17
#----------------------
#s^3 + 6 s^2 + 5 s + 17
If you look at the numerator, you will notice it has a term of . The value 17 corresponds to our parameter , while the value is practically negligible. As a result, we can consider our closed-loop transfer function to be:
Where will be our charateristic equation.
Our next step involves determining a value for that will render the plant marginally stable, thereby promoting sustained oscillation. This can be achieved using Routh's stability criterion. In Python, we can leverage the method outlined in the tbcontrol library to accomplish this.
def routh(p):
coefficients = p.all_coeffs()
N = len(coefficients)
M = sympy.zeros(N, (N+1)//2 + 1)
r1 = coefficients[0::2]
r2 = coefficients[1::2]
M[0, :len(r1)] = [r1]
M[1, :len(r2)] = [r2]
for i in range(2, N):
for j in range(N//2):
S = M[[i-2, i-1], [0, j+1]]
M[i, j] = sympy.simplify(-S.det()/M[i-1,0])
return M[:, :-1]
Then we can implement it like this:
import sympy
Kp = sympy.Symbol('Kp')
s = sympy.Symbol('s')
ce = s**3 + 6*s**2 + 5*s + Kp #charateristic equation
A = routh(sympy.Poly(ce,s))
A
It will give us.
To check the stability limits we can use sympy like this.
sympy.solve([e > 0 for e in A[:,0]], Kp)
Resulting
We have determined that the critical value is 30, which represents the threshold for sustaining oscillation. Accordingly, the critical gain can be expressed as . With this value in place, the characteristic equation takes on the following form:
To find the frequency of the sustained oscillation, we replace getting.
or
where the frequency of the sustained oscillation will be
We can also solve this directly using sympy like this.
s = sympy.Symbol('s')
ce = s**3 + 6*s**2 + 5*s + 30
roots = sympy.solve(ce,s)
roots
Finally, using the ZieglerβNichols Rule, we determine the values of
Where
import numpy as np
w = np.sqrt(5)
Pcr = (2*np.pi)/w
Kcr = 30
Kp = 0.6*Kcr
Ti = 0.5*Pcr
Td = 0.125*Pcr
print("Kp:", Kp)
print("Ti:", Ti)
print("Td:", Td)
#result
#Kp: 18.0
#Ti: 1.40496...
#Td: 0.3512...
Testing the PID
Now that we've tuned our PID controller, the next step is to evaluate the system's behavior. One way to accomplish this is by plotting the step response using the python-control library. This can be achieved as follows:
s = control.TransferFunction.s
plant = (1/(s*(s+1)*(s+5)))
Kp = 18
Ti = 1.405
Td = 0.351
pid = (Kp*(1 + 1/(Ti*s) + Td*s))
feedback = control.feedback(system*pid)
print(feedback)
#result
# 8.877 s^2 + 25.29 s + 18
#----------------------------------------------
#1.405 s^4 + 8.43 s^3 + 15.9 s^2 + 25.29 s + 18
t = np.linspace(0,17)
T, y = control.step_response(feedback, t)
# plot
plt.plot(T, y)
plt.xlabel('Time (seconds)')
plt.ylabel('Output')
plt.title('Step Response')
plt.grid()
plt.show()
You can also get specific information using the step_info method of python-control like the overshoot or RiseTime. etc.
info = control.step_info(feedback)
print("feedback info")
print("-------------")
print("RiseTime:",info["RiseTime"])
print("Overshoot:", info["Overshoot"])
#result
#feedback info
#-------------
#RiseTime: 0.5058786385507097
#Overshoot: 93.70305071110504
So that's all for now! You've seen how Python can be used to design a simple PID controller, using the Ziegler-Nichols method as one of many techniques to determine the PID parameters. Keep in mind that this approach may not always produce optimal values for your specific needs. From here, you can start experimenting with different techniques to fine-tune your controller using the libraries that we use.
You can check the full code here
I hope this has been helpful, and I'll see you soon!
References
-
[1] Katushiko Ogata."Modern Control Engineering fifth Edition". Prentice Hall.
-
[2] Carl Sandrock. "Dynamics and Control with jupyter notebooks".
-
[3] Carl Sandrock. "tbcontrol library".
-
[4] Python-control.org. "Python Control System Library".
-
[5] Sympy Developer Team. Sympy Documentation.
-
[6] PID Controller wikipedia.