I have been working on plotting time varying functions in Python as my most recent project, and would love input on optimization, proper Tkinter form, and anything that stands out like a sore thumb.
To get technical details out of the way, this code has been tested and works on my home box running Linux Mint 18 (4.4.0-21-generic kernel) with Python 3.5.2. It uses matplotlib, numpy, sympy, and Tkinter. The code supplied will be verbatim what I currently have, saved as 3 separate files. The import structure is definitely something I need clarification on!
For use, one can switch between graphs stored via the dropdown box and clicking the 'Make' button. New graphs can be entered by typing in first an initial function (in numpy format, such as np.sin(x)) varying in space (x), and a varying function (eg np.sin(x+i)), varying in space (x) and time (i), and pressing the respective add button. Time progression can be stopped and started via two buttons.
dictionaries.py
from sympy import *
from sympy.parsing.sympy_parser import parse_expr
import Tkinter as Tk
"""
These dictionaries serve the purpose of storing the space varying and
time varying functions to be plotted.
"""
initFuncts = {0: '(np.exp(-0.5 * x ** 2) +'
'2 * x * np.exp(-0.5 * x ** 2)) ** 2',
1: 'x**2'}
varyFuncts = {0: '((1 / (4 * np.pi)) ** (1 / 4)) * '
'((np.exp(-0.5 * x ** 2) * np.cos(0.5 * i) + '
'2 * x * np.exp(-0.5 * x ** 2) *'
'np.cos(1.5 * i)) ** 2)',
1: 'x**2 + np.cos(i)'}
def addInitFunct(textField, options, strings):
initFuncts[len(initFuncts)] = textField.get()
expr = parse_expr(initFuncts[len(initFuncts)-1].replace('np.', ''))
options.children['menu'].delete(0, 'end')
strings.set('')
for key in initFuncts:
options.children['menu'].add_command(
label=str(key),
command=Tk._setit(strings, str(key)))
def addVaryFunct(textField):
varyFuncts[len(varyFuncts)] = textField.get()
animationObj.py
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import Tkinter as Tk, numpy as np
import matplotlib.pyplot as plt, matplotlib.animation as animation
import dictionaries as dicts
class animationObj:
def __init__(self, mainframe, num, x, timeString):
"""
Create instance of animationObj class, enabling the animation of
matplotlib based figures.
Keyword arguments:
mainframe -- the frame the animation will appear in
num -- the function number in the dictionaries
x -- spacial range of the animation
timeString -- string which will report current time to user
"""
self._master = mainframe # set frame
self.timeString, self.num, self.x = timeString, num, x
self.fig = plt.Figure() # create pyplot figure
# add figure to canvas and connect canvas to frame
self.canvas = FigureCanvasTkAgg(self.fig, master=self._master)
# get widget then pack
self.tkwidget = self.canvas.get_tk_widget()
self.tkwidget.pack()
# add subplot to actually animate on
self.ax = self.fig.add_subplot(111)
# generate initial plot, note the syntax: 'self.line,', comma
# after variable means returning as a tuple
self.line, = self.ax.plot(self.x,
eval(dicts.initFuncts[self.num]))
# set bounds on plot
self.ax.axis([-6, 6, -5, 5])
# animate the animation
self.ani = animation.FuncAnimation(self.fig, self.animate,
np.arange(0, 200, 0.01),
interval=1, blit=False)
def animate(self, i):
"""
Animation function which executes every 'interval given in the
FuncAnimation instantiation.
Keyword arguments:
i -- the current time
"""
# must create a (not technically called this...) 'local global'
# version of x because set_ydata expects a global variable x
x = self.x
self.timeString.set('t = ' + '{:3.2f}'.format(i)) # report time
# generate plot at next time interval
self.line.set_ydata(eval(dicts.varyFuncts[self.num]))
# return line tuple
return self.line,
def stopIt(self):
"""Pause the current animation shown."""
self.ani.event_source.stop()
def startIt(self):
"""Resume the current animation shown."""
self.ani.event_source.start()
def removeIt(self):
"""Remove the current animation from the window."""
self.tkwidget.pack_forget()
self.ani.event_source.stop()
def addIt(self, num):
"""
Add a new plot to the plotting window.
Keyword arguments:
num -- the function number in the dictionaries
"""
if (num in dicts.initFuncts) and (num in dicts.varyFuncts):
self.removeIt() # remove old plot
x = self.x # same as before, expects a global x
i = 0 # reset time to zero
self.num = int(num) # update instance variable
#reinitialize all needed components
self.fig = plt.Figure()
self.canvas = FigureCanvasTkAgg(self.fig, master=self._master)
self.tkwidget = self.canvas.get_tk_widget()
self.tkwidget.pack()
self.ax = self.fig.add_subplot(111)
self.line, = self.ax.plot(self.x,
eval(dicts.initFuncts[self.num]))
self.ax.axis([-6, 6, -5, 5])
self.ani = animation.FuncAnimation(self.fig, self.animate,
np.arange(0, 200, 0.01),
interval=1, blit=False)
main.py
import Tkinter as Tk
import numpy as np
import animationObj
import dictionaries
def _quit(mast):
"""
Quits the entire application.
Keyword arguments:
mast -- the root Tk instance
"""
mast.quit()
mast.destroy()
root = Tk.Tk() # root Tk instance
t1 = Tk.Toplevel(root)
t2 = Tk.Toplevel(root) # Tk Toplevel instance to separate windows
# add frames to windows
f1 = Tk.Frame(t1)
f2 = Tk.Frame(t2)
f = Tk.Frame(root)
label1 = Tk.Label(f, text="Superposition State").pack()
# set each frames' geometry
t2.geometry("%dx%d+%d+%d" % (250, 300, 650, 625))
t1.geometry("%dx%d+%d+%d" % (250, 300, 650, 300))
root.geometry("%dx%d+%d+%d" % (500, 500, 150, 300))
# drop down menu
options = Tk.StringVar()
options.set('0')
w = Tk.OptionMenu(f1, options, '0', '1')
w.pack(side=Tk.BOTTOM)
# string to display time
timeString = Tk.StringVar()
timeString.set('t = 0')
l = Tk.Label(f1, textvariable=timeString, font=("Courier", 24))
# make and pack quit button
button = Tk.Button(master=f1, text='Quit',
command=lambda: _quit(root), height=2)
button.pack(side=Tk.BOTTOM)
# pack time string
l.pack(side=Tk.TOP)
x = np.arange(-6, 6, 0.005) # range of x values
aniObj = animationObj.animationObj(f, 0, x, timeString)
# other buttons
button3 = Tk.Button(master=f1, text='Make',
command=lambda: aniObj.addIt(int(options.get())), height=2)
stopIt = Tk.Button(master=f1, text='Stop', command=aniObj.stopIt, height=1)
startIt = Tk.Button(master=f1, text='Start',
command=aniObj.startIt, height=1)
# pack frames
f.pack()
f1.pack()
f2.pack()
# pack buttons
button3.pack(side=Tk.BOTTOM)
stopIt.pack(side=Tk.LEFT)
startIt.pack(side=Tk.LEFT)
e = Tk.Entry(f2)
button4 = Tk.Button(master=f2, text='Add Initial Function',
command=lambda: dictionaries.addInitFunct(e, w, options),
height=1)
button5 = Tk.Button(master=f2, text='Add Varying Function', command=lambda:
dictionaries.addVaryFunct(e), height=1)
button4.pack(side=Tk.BOTTOM)
button5.pack(side=Tk.BOTTOM)
e.pack(side=Tk.TOP)
# execute main loop on base instance
root.mainloop()
Again I would like to thank any advice and criticisms offered up in advance!