Skip to content

Commit 96cba52

Browse files
committed
Added tfvis.py, a simple GUI application for visualizing how the
poles/zeros of the transfer function effects the bode, nyquist and step response of a SISO system. Contributed by Vanessa Romero Segovia, Ola Johnsson, Jerker Nordh. Changed logspace import to pull this from the control.matlab instead of numpy (not found in numpy 2.0.0 on python 2.6.1).
1 parent ac8279d commit 96cba52

File tree

1 file changed

+375
-0
lines changed

1 file changed

+375
-0
lines changed

examples/tfvis.py

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
#!/usr/bin/python
2+
""" Simple GUI application for visualizing how the poles/zeros of the transfer
3+
function effects the bode, nyquist and step response of a SISO system """
4+
5+
"""Copyright (c) 2011, All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without
8+
modification, are permitted provided that the following conditions
9+
are met:
10+
11+
1. Redistributions of source code must retain the above copyright
12+
notice, this list of conditions and the following disclaimer.
13+
14+
2. Redistributions in binary form must reproduce the above copyright
15+
notice, this list of conditions and the following disclaimer in the
16+
documentation and/or other materials provided with the distribution.
17+
18+
3. Neither the name of the project author nor the names of its
19+
contributors may be used to endorse or promote products derived
20+
from this software without specific prior written permission.
21+
22+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH
26+
OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
29+
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
30+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32+
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33+
SUCH DAMAGE.
34+
35+
Author: Vanessa Romero Segovia
36+
Author: Ola Johnsson
37+
Author: Jerker Nordh
38+
"""
39+
40+
import control.matlab
41+
import Tkinter
42+
import sys
43+
import Pmw
44+
import matplotlib.pyplot as plt
45+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
46+
from numpy.lib.polynomial import polymul
47+
from numpy.lib.type_check import real
48+
from numpy.core.multiarray import array
49+
from numpy.core.fromnumeric import size
50+
# from numpy.lib.function_base import logspace
51+
from control.matlab import logspace
52+
from numpy import conj
53+
54+
def make_poly(facts):
55+
""" Create polynomial from factors """
56+
poly = [1]
57+
for factor in facts:
58+
poly = polymul(poly, [1, -factor])
59+
60+
return real(poly)
61+
62+
def coeff_string_check(text):
63+
""" Check so textfield entry is valid string of coeffs. """
64+
try:
65+
[float(a) for a in text.split()]
66+
except:
67+
return Pmw.PARTIAL
68+
69+
return Pmw.OK
70+
71+
class TFInput:
72+
""" Class for handling input of transfer function coeffs."""
73+
def __init__(self, parent):
74+
self.master = parent
75+
self.denominator = []
76+
self.numerator = []
77+
self.numerator_widget = Pmw.EntryField(self.master,
78+
labelpos='w',
79+
label_text='Numerator',
80+
entry_width = 25,
81+
validate=coeff_string_check,
82+
value='1.0 -6.0 12.0')
83+
self.denominator_widget = Pmw.EntryField(self.master,
84+
labelpos='w',
85+
label_text='Denominator',
86+
entry_width = 25,
87+
validate=coeff_string_check,
88+
value='1.0 5.0 14.0 27.0')
89+
self.balloon = Pmw.Balloon(self.master)
90+
91+
try:
92+
self.balloon.bind(self.numerator_widget,
93+
"Numerator coefficients, e.g: 1.0 2.0")
94+
except:
95+
pass
96+
97+
try:
98+
self.balloon.bind(self.denominator_widget,
99+
"Denominator coefficients, e.g: 1.0 3.0 2.0")
100+
except:
101+
pass
102+
103+
widgets = (self.numerator_widget, self.denominator_widget)
104+
for i in xrange(len(widgets)):
105+
widgets[i].grid(row=i+1, column=0, padx=20, pady=3)
106+
Pmw.alignlabels(widgets)
107+
108+
self.numerator_widget.component('entry').focus_set()
109+
110+
def get_tf(self):
111+
""" Return transfer functions object created from coeffs"""
112+
try:
113+
numerator = (
114+
[float(a) for a in self.numerator_widget.get().split()])
115+
except:
116+
numerator = None
117+
118+
try:
119+
denominator = (
120+
[float(a) for a in self.denominator_widget.get().split()])
121+
except:
122+
denominator = None
123+
124+
try:
125+
if (numerator != None and denominator != None):
126+
tfcn = control.matlab.tf(numerator, denominator)
127+
else:
128+
tfcn = None
129+
except:
130+
tfcn = None
131+
132+
return tfcn
133+
134+
135+
136+
def set_poles(self, poles):
137+
""" Set the poles to the new positions"""
138+
self.denominator = make_poly(poles)
139+
self.denominator_widget.setentry(
140+
' '.join([format(i,'.3g') for i in self.denominator]))
141+
142+
def set_zeros(self, zeros):
143+
""" Set the zeros to the new positions"""
144+
self.numerator = make_poly(zeros)
145+
self.numerator_widget.setentry(
146+
' '.join([format(i,'.3g') for i in self.numerator]))
147+
148+
class Analysis:
149+
""" Main class for GUI visualising transfer functions """
150+
def __init__(self, parent):
151+
"""Creates all widgets"""
152+
self.master = parent
153+
self.move_zero = None
154+
self.index1 = None
155+
self.index2 = None
156+
self.zeros = []
157+
self.poles = []
158+
159+
self.topframe = Tkinter.Frame(self.master)
160+
self.topframe.pack(expand=True, fill='both')
161+
162+
self.entries = Tkinter.Frame(self.topframe)
163+
self.entries.pack(expand=True, fill='both')
164+
165+
self.figure = Tkinter.Frame(self.topframe)
166+
self.figure.pack(expand=True, fill='both')
167+
168+
header = Tkinter.Label(self.entries,
169+
text='Define the transfer function:')
170+
header.grid(row=0, column=0, padx=20, pady=7)
171+
172+
173+
self.tfi = TFInput(self.entries)
174+
self.sys = self.tfi.get_tf()
175+
176+
Tkinter.Button(self.entries, text='Apply', command=self.apply,
177+
width=9).grid(row=0, column=1, rowspan=3, padx=10, pady=5)
178+
179+
self.f_bode = plt.figure(figsize=(4, 4))
180+
self.f_nyquist = plt.figure(figsize=(4, 4))
181+
self.f_pzmap = plt.figure(figsize=(4, 4))
182+
self.f_step = plt.figure(figsize=(4, 4))
183+
184+
self.canvas_pzmap = FigureCanvasTkAgg(self.f_pzmap,
185+
master=self.figure)
186+
self.canvas_pzmap.show()
187+
self.canvas_pzmap.get_tk_widget().grid(row=0, column=0,
188+
padx=0, pady=0)
189+
190+
self.canvas_bode = FigureCanvasTkAgg(self.f_bode,
191+
master=self.figure)
192+
self.canvas_bode.show()
193+
self.canvas_bode.get_tk_widget().grid(row=0, column=1,
194+
padx=0, pady=0)
195+
196+
self.canvas_step = FigureCanvasTkAgg(self.f_step,
197+
master=self.figure)
198+
self.canvas_step.show()
199+
self.canvas_step.get_tk_widget().grid(row=1, column=0,
200+
padx=0, pady=0)
201+
202+
self.canvas_nyquist = FigureCanvasTkAgg(self.f_nyquist,
203+
master=self.figure)
204+
self.canvas_nyquist.show()
205+
self.canvas_nyquist.get_tk_widget().grid(row=1, column=1,
206+
padx=0, pady=0)
207+
208+
self.canvas_pzmap.mpl_connect('button_press_event',
209+
self.button_press)
210+
self.canvas_pzmap.mpl_connect('button_release_event',
211+
self.button_release)
212+
self.canvas_pzmap.mpl_connect('motion_notify_event',
213+
self.mouse_move)
214+
215+
self.apply()
216+
217+
def button_press(self, event):
218+
""" Handle button presses, detect if we are going to move
219+
any poles/zeros"""
220+
# find closest pole/zero
221+
if (event.xdata != None and event.ydata != None):
222+
223+
new = event.xdata + 1.0j*event.ydata
224+
225+
tzeros = list(abs(self.zeros-new))
226+
tpoles = list(abs(self.poles-new))
227+
228+
if (size(tzeros) > 0):
229+
minz = min(tzeros)
230+
else:
231+
minz = float('inf')
232+
if (size(tpoles) > 0):
233+
minp = min(tpoles)
234+
else:
235+
minp = float('inf')
236+
237+
if (minz < 2 or minp < 2):
238+
if (minz < minp):
239+
# Moving zero(s)
240+
self.index1 = tzeros.index(minz)
241+
self.index2 = list(self.zeros).index(
242+
conj(self.zeros[self.index1]))
243+
self.move_zero = True
244+
else:
245+
# Moving pole(s)
246+
self.index1 = tpoles.index(minp)
247+
self.index2 = list(self.poles).index(
248+
conj(self.poles[self.index1]))
249+
self.move_zero = False
250+
251+
def button_release(self, event):
252+
""" Handle button release, update pole/zero positions,
253+
if the were moved"""
254+
if (self.move_zero == True):
255+
self.tfi.set_zeros(self.zeros)
256+
elif (self.move_zero == False):
257+
self.tfi.set_poles(self.poles)
258+
else:
259+
return
260+
261+
self.move_zero = None
262+
self.index1 = None
263+
self.index2 = None
264+
265+
tfcn = self.tfi.get_tf()
266+
if (tfcn):
267+
self.zeros = tfcn.zero()
268+
self.poles = tfcn.pole()
269+
self.sys = tfcn
270+
self.redraw()
271+
272+
def mouse_move(self, event):
273+
""" Handle mouse movement, redraw pzmap while drag/dropping """
274+
if (self.move_zero != None and
275+
event.xdata != None and
276+
event.ydata != None):
277+
278+
if (self.index1 == self.index2):
279+
# Real pole/zero
280+
new = event.xdata
281+
if (self.move_zero == True):
282+
self.zeros[self.index1] = new
283+
elif (self.move_zero == False):
284+
self.poles[self.index1] = new
285+
else:
286+
# Complex poles/zeros
287+
new = event.xdata + 1.0j*event.ydata
288+
if (self.move_zero == True):
289+
self.zeros[self.index1] = new
290+
self.zeros[self.index2] = conj(new)
291+
elif (self.move_zero == False):
292+
self.poles[self.index1] = new
293+
self.poles[self.index2] = conj(new)
294+
tfcn = None
295+
if (self.move_zero == True):
296+
self.tfi.set_zeros(self.zeros)
297+
tfcn = self.tfi.get_tf()
298+
elif (self.move_zero == False):
299+
self.tfi.set_poles(self.poles)
300+
tfcn = self.tfi.get_tf()
301+
if (tfcn != None):
302+
self.draw_pz(tfcn)
303+
self.canvas_pzmap.show()
304+
305+
def apply(self):
306+
"""Evaluates the transfer function and produces different plots for
307+
analysis"""
308+
tfcn = self.tfi.get_tf()
309+
310+
if (tfcn):
311+
self.zeros = tfcn.zero()
312+
self.poles = tfcn.pole()
313+
self.sys = tfcn
314+
self.redraw()
315+
316+
def draw_pz(self, tfcn):
317+
"""Draw pzmap"""
318+
self.f_pzmap.clf()
319+
# Make adaptive window size, with min [-10, 10] in range,
320+
# always atleast 25% extra space outside poles/zeros
321+
tmp = list(self.zeros)+list(self.poles)+[8]
322+
val = 1.25*max(abs(array(tmp)))
323+
plt.figure(self.f_pzmap.number)
324+
control.matlab.pzmap(tfcn)
325+
plt.suptitle('Pole-Zero Diagram')
326+
327+
plt.axis([-val, val, -val, val])
328+
329+
def redraw(self):
330+
""" Redraw all diagrams """
331+
self.draw_pz(self.sys)
332+
333+
self.f_bode.clf()
334+
plt.figure(self.f_bode.number)
335+
control.matlab.bode(self.sys, logspace(-2, 2))
336+
plt.suptitle('Bode Diagram')
337+
338+
self.f_nyquist.clf()
339+
plt.figure(self.f_nyquist.number)
340+
control.matlab.nyquist(self.sys, logspace(-2, 2))
341+
plt.suptitle('Nyquist Diagram')
342+
343+
self.f_step.clf()
344+
plt.figure(self.f_step.number)
345+
try:
346+
# Step seems to get intro trouble
347+
# with purely imaginary poles
348+
tvec, yvec = control.matlab.step(self.sys)
349+
plt.plot(tvec.T, yvec)
350+
except:
351+
print "Error plotting step response"
352+
plt.suptitle('Step Response')
353+
354+
self.canvas_pzmap.show()
355+
self.canvas_bode.show()
356+
self.canvas_step.show()
357+
self.canvas_nyquist.show()
358+
359+
def create_analysis():
360+
""" Create main object """
361+
def handler():
362+
""" Handles WM_DELETE_WINDOW messages """
363+
root.destroy()
364+
sys.exit()
365+
366+
# Launch a GUI for the Analysis module
367+
root = Tkinter.Tk()
368+
root.protocol("WM_DELETE_WINDOW", handler)
369+
Pmw.initialise(root)
370+
root.title('Analysis of Linear Systems')
371+
Analysis(root)
372+
root.mainloop()
373+
374+
if __name__ == '__main__':
375+
create_analysis()

0 commit comments

Comments
 (0)