Skip to content

Instantly share code, notes, and snippets.

@carlos-adir
Last active September 26, 2020 21:34
Show Gist options
  • Save carlos-adir/f66e581734c7a9b661b9f9211b8ce567 to your computer and use it in GitHub Desktop.
Save carlos-adir/f66e581734c7a9b661b9f9211b8ce567 to your computer and use it in GitHub Desktop.
Ploting curves of Bode, Nichols and Nyquist using Tkinter as GUI and sympy to treat the expressions
##########################
# Ploting Control Graphs #
##########################
1. Esse é um código desenvolvido para fins de aprendizado, use-o conforme queira. Aproveite bem!
2. Para usa-lo, você precisa ter alguns requisitos instalados:
2.1) python3 - Linguagem de programação
2.2) numpy - Biblioteca de calculo do python
2.3) sympy - Biblioteca de calculo simbólico do python
2.4) control - Biblioteca para o calculo dos valores de ganho de fase, frequência e por aí vai.
2.5) tkinter - Biblioteca para fazer a parte gráfica, de interação com o usuário, com botões e entradas
2.6) matplotlib - Biblioteca para plotar os gráficos
Se você já instalar o Anaconda, que já vem com diversas bibliotecas dessas, você não encontrará nenhum problema.
3. Existem 3 variáveis no sistema, de nomes A, B e K nessa ordem. Você pode usa-las e alterar o valor como quiser.
Cada slider varia de 0 até 100, mas cada variável vai de 0 até 1. Ou seja, com o slider no 0, a variável vale 0, com o slider no 100, o slider vale 1. Com o slider no 50, a variável vale 0.5, e assim por diante.
Caso você precise de valores maiores, coloque na equação. Por exemplo, se precisar que slider varie de 0 até 50, então coloque A*50.
4. Se você clicar no exemplo do ultimo botão, ele colocará uma função de transferência qualquer. Você pode alterar os sliders e ver as alterações que acontece nos gráficos. Você também consegue alterar valores numéricos das funções.
import tkinter as tk
import negocio
import numpy as np
import matplotlib.ticker as ticker
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from matplotlib.figure import Figure
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2TkAgg)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
def create_new_window(master, classe, *args):
Window = tk.Toplevel(master)
if classe == "BodeGain":
app = Window_BodeGainGraph(Window, args)
elif classe == "BodePhase":
app = Window_BodeGainPhase(Window, args)
elif classe == "Nichols":
app = Window_NicholsGraph(Window, args)
elif classe == "Nyquist":
app = Window_NyquistGraph(Window, args)
return app
class Window_Parameters:
def __init__(self, master):
self.master = master
self.frame_Entry = tk.Frame(self.master)
self.frame_Sliders = tk.Frame(self.master)
self.frame_Buttons = tk.Frame(self.master)
self.frame_Test = tk.Frame(self.master)
self.__init_Entry(self.frame_Entry)
self.__init_Sliders(self.frame_Sliders)
self.__init_Buttons(self.frame_Buttons)
self.__init_Test(self.frame_Test)
self.frame_Entry.pack()
self.frame_Sliders.pack()
self.frame_Buttons.pack()
self.frame_Test.pack()
self.master.title("Parameters")
def __init_Entry(self, frame):
######### Entry for Transfer function
if 1: # Numerador
tk.Label(frame, text="num: ").pack()
self.var_Numerator = tk.StringVar()
self.entry_Numerator = tk.Entry(frame, textvariable=self.var_Numerator, justify='center')
self.entry_Numerator.pack()
self.var_Numerator.trace('w', self.__changeEntry_Numerator) # The 'w' tells tkinter whenever somebody writes (updates) the variable, which would happen every time someone wrote something in the Entry widget, do __entryNumeratorFunctionChange.
if 1: # Denominator
tk.Label(frame, text="den: ").pack()
self.var_Denominator = tk.StringVar()
self.entry_Denominator = tk.Entry(frame, textvariable=self.var_Denominator, justify='center')
self.entry_Denominator.pack()
self.var_Denominator.trace('w', self.__changeEntry_Denominator) # The 'w' tells tkinter whenever somebody writes (updates) the variable, which would happen every time someone wrote something in the Entry widget, do __entryNumeratorFunctionChange.
def __init_Sliders(self, frame):
######### Sliders for variables
self.sliders = {}
for variable in negocio.slider_infos:
name = variable.get_name()
begin = variable.get_begin()
end = variable.get_end()
value = variable.get_value()
resolution = variable.get_resolution()
var = tk.DoubleVar()
slider = tk.Scale(frame, variable=var, from_=begin, to=end, resolution=resolution, orient=tk.HORIZONTAL)
slider.set(value)
slider.pack()
if name == "A":
func = self.__changeSlider_A
if name == "B":
func = self.__changeSlider_B
if name == "K":
func = self.__changeSlider_K
var.trace('w', func)
self.sliders[name] = (var, slider)
def __init_Buttons(self, frame):
######### Ploting Graphs
functions = {}
titles = {}
self.buttons = {}
names = ["BodeGain", "BodePhase", "Nichols", "Nyquist"]
functions["BodeGain"] = self.__changeButton_BodeGain
functions["BodePhase"] = self.__changeButton_BodePhase
functions["Nichols"] = self.__changeButton_Nichols
functions["Nyquist"] = self.__changeButton_Nyquist
titles["BodeGain"] = "Plot de " + str("BodeGain")
titles["BodePhase"] = "Plot de " + str("BodePhase")
titles["Nichols"] = "Plot de " + str("Nichols")
titles["Nyquist"] = "Plot de " + str("Nyquist")
for name in names:
self.buttons[name] = tk.Button(frame, text = titles[name], width = 25,
command = functions[name])
self.buttons[name].pack()
def __init_Test(self, frame):
######### Test widget
if 1: # Button for Bode Gain Graph
self.button_Test = tk.Button(frame, text = 'Exemplo', width = 25, command = self.test_function)
self.button_Test.pack()
def __changeSlider_A(self, *args):
name = "A"
var, slider = self.sliders[name]
value = slider.get()
#print("type(A) = " + str(type(A)) + " A = " + str(A))
manager.callbackA(value)
def __changeSlider_B(self, *args):
name = "B"
var, slider = self.sliders[name]
value = slider.get()
manager.callbackB(value)
def __changeSlider_K(self, *args):
name = "K"
var, slider = self.sliders[name]
value = slider.get()
manager.callbackK(value)
def __changeEntry_Numerator(self, *arg):
expr = self.entry_Numerator.get()
#print("Getting " + str(expr) + " from entry_Numerator")
manager.callbackNum(expr)
def __changeEntry_Denominator(self, *arg):
expr = self.entry_Denominator.get()
#print("Getting " + str(expr) + " from entry_Numerator")
manager.callbackDen(expr)
def set_stateEntry_Numerator(self, state):
if state == True: # Bom estado, correto, valido
self.entry_Numerator.config({"background": "#bbffbb"})
else:
self.entry_Numerator.config({"background": "#ffbbbb"})
def set_stateEntry_Denominator(self, state):
if state == True: # Bom estado, correto, valido
self.entry_Denominator.config({"background": "#bbffbb"})
else:
self.entry_Denominator.config({"background": "#ffbbbb"})
def __changeButton_BodeGain(self):
name = "BodeGain"
classe = classes[name]
if manager.can_new_window(name):
Window = tk.Toplevel(self.master)
app = classe(Window)
manager.set_app(name, app)
def __changeButton_BodePhase(self):
name = "BodePhase"
classe = classes[name]
if manager.can_new_window(name):
Window = tk.Toplevel(self.master)
app = classe(Window)
manager.set_app(name, app)
def __changeButton_Nichols(self):
name = "Nichols"
classe = classes[name]
if manager.can_new_window(name):
Window = tk.Toplevel(self.master)
app = classe(Window)
manager.set_app(name, app)
def __changeButton_Nyquist(self):
name = "Nyquist"
classe = classes[name]
if manager.can_new_window(name):
Window = tk.Toplevel(self.master)
app = classe(Window)
manager.set_app(name, app)
def test_function(self):
self.entry_Numerator.delete(0, tk.END)
self.entry_Numerator.insert(0, "1.2707*(1+s)*K")
self.entry_Denominator.delete(0, tk.END)
self.entry_Denominator.insert(0, "1.2707*(1+s)+(1+A*s)*(0.044212*s^2+0.41258*s+1)")
class Window_BodeGainGraph(Gtk.Window):
def __init__(self, master):
self.master = master
self.master.title("Bode Gain Graph")
self.frame = tk.Frame(self.master)
self.frame.pack()
self.x = np.logspace(-3, 3, 128)
self.y = np.log(self.x)
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
self.lines, = self.ax.semilogx(self.x, self.y)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.frame)
self.toolbar.update()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.ax.set_xlim([10**(-4), 10**(4)])
self.ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
self.ax.grid()
def update(self, w, gain, marginGain, marginPhase):
try:
mingain, maxgain = min(gain), max(gain)
if maxgain-mingain < 40:
dgain = 40
else:
dgain = maxgain - mingain
ymin = mingain - dgain/4
ymax = maxgain + dgain/4
self.lines.set_xdata(w)
self.lines.set_ydata(gain)
self.canvas.draw_idle()
self.ax.set_ylim([ymin, ymax])
except:
print("Problema no update do BodeGain")
def close_windows(self):
manager.set_app("BodeGain", None)
self.master.destroy()
class Window_BodePhaseGraph:
def __init__(self, master):
self.master = master
self.master.title("Bode Phase Graph")
self.frame = tk.Frame(self.master)
self.frame.pack()
self.x = np.logspace(-3, 3, 128)
self.y = np.log(self.x)
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
self.lines, = self.ax.semilogx(self.x, self.y)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.frame)
self.toolbar.update()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.ax.set_xlim([10**(-4), 10**(4)])
self.ax.yaxis.set_major_locator(ticker.MultipleLocator(45))
self.ax.grid()
def update(self, w, phase, marginGain, marginPhase):
minphase, maxphase = min(phase), max(phase)
dphase = maxphase-minphase
self.lines.set_xdata(w)
self.lines.set_ydata(phase)
self.canvas.draw_idle()
self.ax.set_ylim([minphase-dphase/4, maxphase+dphase/4])
def close_windows(self):
manager.app_BodePhase = None
self.master.destroy()
class Window_NicholsGraph:
def __init__(self, master):
self.master = master
self.master.title("Nichols Graph")
self.frame = tk.Frame(self.master)
self.frame.pack()
self.x = np.linspace(-360, 0, 128)
self.y = self.x
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
self.lines, = self.ax.plot(self.x, self.y)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.frame)
self.toolbar.update()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.ax.set_xlim([-360, 0])
self.ax.xaxis.set_major_locator(ticker.MultipleLocator(45))
self.ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
self.ax.grid()
def update(self, phase, gain, marginGain, marginPhase):
mingain, maxgain = min(gain), max(gain)
if maxgain-mingain < 40:
dgain = 40
else:
dgain = maxgain - mingain
ymin = mingain - dgain/4
ymax = maxgain + dgain/4
self.lines.set_xdata(phase)
self.lines.set_ydata(gain)
self.canvas.draw_idle()
self.ax.set_ylim([ymin, ymax])
def close_windows(self):
manager.app_BodePhase = None
self.master.destroy()
class Window_NyquistGraph:
def __init__(self, master):
self.master = master
self.master.title("Nyquist Graph")
self.frame = tk.Frame(self.master)
self.frame.pack()
self.x = np.linspace(-3, 3, 128)
self.y = self.x
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
self.lines1, = self.ax.plot(self.x, self.y, color='b')
self.lines2, = self.ax.plot(self.x, -self.y, color='b')
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.frame)
self.toolbar.update()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#self.ax.xaxis.set_major_locator(ticker.MultipleLocator(45))
#self.ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
self.ax.grid()
def update(self, real, imag, marginGain, marginPhase, poles, zeros):
if len(poles) > 0:
maxp = max(abs(poles))
else:
maxp = 1
if len(zeros) > 0:
maxz = max(abs(zeros))
else:
maxz = 1
maxr = max(abs(real))
maxi = max(abs(imag))
if maxi > 5*maxr:
maxi = 0
unit = max(maxz, maxp, maxr, maxi)
self.lines1.set_xdata( real)
self.lines1.set_ydata( imag)
self.lines2.set_xdata( real)
self.lines2.set_ydata(-imag)
self.canvas.draw_idle()
self.ax.set_xlim([-1.2*unit, +1.2*unit])
self.ax.set_ylim([-1.2*unit, +1.2*unit])
def close_windows(self):
manager.app_BodePhase = None
self.master.destroy()
classes = {}
classes["BodeGain"] = Window_BodeGainGraph
classes["BodePhase"] = Window_BodePhaseGraph
classes["Nichols"] = Window_NicholsGraph
classes["Nyquist"] = Window_NyquistGraph
if __name__ == '__main__':
root = tk.Tk()
app = Window_Parameters(root)
manager = negocio.ManagerParameter(app)
root.mainloop()
import ast
import sympy as sp
from sympy.abc import s
from sympy.parsing.sympy_parser import parse_expr
import matplotlib.pyplot as plt
from scipy import signal
import control
import numpy as np
class Calcul:
def treatStr(string):
expr = expr.replace("^", "**")
if ',' in expr:
raise TypeError("Virgula nao eh separador decimal")
def strToSympyExpr(string):
expr = parse_expr(expr) # transform string to sympy expression
expr = expr.expand()
for var, value in variables.items():
expr = expr.subs(var, value)
expr = expr.simplify()
def separateFraction(expr):
num, den = sp.fraction(expr)
return num, den
def convertExprToCoefs(expr, variables, verbose = False):
if verbose: print("hum1")
expr = expr.replace("^", "**")
if ',' in expr:
raise TypeError("Virgula nao eh separador decimal")
if verbose: print("hum2")
expr = parse_expr(expr) # transform string to sympy expression
if verbose: print("hum3")
expr = expr.expand()
if verbose: print("hum4")
for var, value in variables.items():
if verbose: print("...")
expr = expr.subs(var, value)
if verbose: print("hum5")
expr = sp.Poly(expr, s)
if verbose: print("hum6")
expr = expr.all_coeffs()
if verbose: print("hum7")
for i, item in enumerate(expr):
expr[i] = float(expr[i])
if verbose: print("type = " + str(type(item)) + " value = " + str(item))
if verbose: print("hum8")
if verbose: print(expr)
return expr
def strToNumDen(expr, variables):
expr = Calcul.treatStr(expr)
expr = Calcul.strToSympyExpr(expr)
num, den = Calcul.separateFraction(expr)
num = sp.Poly(num, s)
num = num.all_coeffs()
den = sp.Poly(den, s)
den = den.all_coeffs()
for i, item in enumerate(expr):
num[i] = float(num[i])
for i, item in enumerate(expr):
den[i] = float(den[i])
return expr
def getData_GainPhase(Gnum, Gden):
w = np.logspace(-4, 4, 128)
sys = signal.lti(Gnum, Gden)
w, mag, phase = signal.bode(sys, w=w)
return w, mag, phase
def getData_Cartesian(Gnum, Gden):
w = np.logspace(-4, 4, 512)
sys = signal.lti(Gnum, Gden)
w, H = signal.freqresp(sys, w=w)
return w, H.real, H.imag
def getData_Margins(Gnum, Gden):
sys = control.tf(Gnum, Gden)
gm, pm, wg, wp = control.margin(sys)
return (wg, gm, -180), (wp, 1, pm-180)
def getData_PolesZeros(Gnum, Gden):
zs, ps, gain = signal.tf2zpk(Gnum, Gden)
return ps, zs
class Variavel:
def __init__(self, name, interval):
self.name = name
self.interval = interval
def get_name(self):
return self.name
def get_begin(self):
return self.interval[0]
def get_end(self):
return self.interval[1]
def get_value(self):
return (self.interval[0]+self.interval[1])/2.0
def get_resolution(self):
return (self.interval[1]-self.interval[0])/100
class ManagerParameter:
def __init__(self, parameter):
self.app = {}
self.app["Parameter"] = parameter
self.app["BodeGain"] = None
self.app["BodePhase"] = None
self.app["Nichols"] = None
self.app["Nyquist"] = None
self.variables = {}
for var in slider_infos:
self.variables[var.get_name()] = var.get_value()
self.validNumerator = False
self.validDenominator = False
self.Gnum = []
self.Gden = []
self.coefsNumerator = []
self.coefsDenominator = []
self.exprNum = ""
self.exprDen = ""
def callbackA(self, X):
self.variables["A"] = X/100
self.__updateNum()
self.__updateDen()
self.__updateTransferFunction()
def callbackB(self, X):
self.variables["B"] = X/100
self.__updateNum()
self.__updateDen()
self.__updateTransferFunction()
def callbackK(self, X):
self.variables["K"] = X/100
self.__updateNum()
self.__updateDen()
self.__updateTransferFunction()
def send_info(self):
if self.app["BodeGain"] != None:
self.app["BodeGain"].update(self.w_g, self.gain, self.marginGain, self.marginPhase)
if self.app["BodePhase"] != None:
self.app["BodePhase"].update(self.w_g, self.phase, self.marginGain, self.marginPhase)
if self.app["Nichols"] != None:
self.app["Nichols"].update(self.phase, self.gain, self.marginGain, self.marginPhase)
if self.app["Nyquist"] != None:
self.app["Nyquist"].update(self.real, self.imag, self.marginGain, self.marginPhase, self.poles, self.zeros)
def __updateTransferFunction(self):
if self.validNumerator and self.validDenominator:
if self.Gnum != self.coefsNumerator or self.Gden != self.coefsDenominator:
self.Gnum = self.coefsNumerator
self.Gden = self.coefsDenominator
try:
#print("getting...")
#print(" - Gain/Phase")
self.w_g, self.gain, self.phase = Calcul.getData_GainPhase(self.Gnum, self.Gden)
#print(" - Nyquist")
self.w_c, self.real, self.imag = Calcul.getData_Cartesian(self.Gnum, self.Gden)
#print(" - Margins")
self.marginGain, self.marginPhase = Calcul.getData_Margins(self.Gnum, self.Gden)
#print(" - Poles and Zeros")
self.poles, self.zeros = Calcul.getData_PolesZeros(self.Gnum, self.Gden)
print(" margin Gain: " + str(self.marginGain))
print("margin Phase: " + str(self.marginPhase))
except:
pass
#print("Deu problema dentro de calcular algum dos parametros dentro da funcao de transferencia")
self.send_info()
#except:
# print("Deu problema na hora de passar os argumentos por update")
def callbackNum(self, expr):
self.exprNum = expr
self.__updateNum()
self.__updateTransferFunction()
def callbackDen(self, expr):
self.exprDen = expr
self.__updateDen()
self.__updateTransferFunction()
def __updateNum(self):
try:
new = Calcul.convertExprToCoefs(self.exprNum, self.variables)
if ( len(self.coefsNumerator) == 0 ) or not all(s == 0 for s in new):
self.coefsNumerator = new
self.validNumerator = True
except (TypeError, SyntaxError, sp.parsing.sympy_tokenize.TokenError) as e:
self.validNumerator = False
self.app["Parameter"].set_stateEntry_Numerator(self.validNumerator)
def __updateDen(self):
try:
new = Calcul.convertExprToCoefs(self.exprDen, self.variables)
if len(self.coefsDenominator) == 0 or not all(s == 0 for s in new):
self.coefsDenominator = new
self.validDenominator = True
except (TypeError, SyntaxError, sp.parsing.sympy_tokenize.TokenError) as e:
self.validDenominator = False
self.app["Parameter"].set_stateEntry_Denominator(self.validDenominator)
def can_new_window(self, name):
return self.app[name] == None and self.validNumerator and self.validDenominator
def set_app(self, name, value):
self.app[name] = value
self.send_info()
slider_infos = []
slider_infos.append(Variavel("A", (0, 100)))
slider_infos.append(Variavel("B", (0, 100)))
slider_infos.append(Variavel("K", (0, 100)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment