2
\$\begingroup\$

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!

\$\endgroup\$
2
\$\begingroup\$

There are a lot of PEP 8 issues in your code. I recommend reading through that document.

It is recommend to avoid using eval, as it makes your code harder to maintain and has lots of security issues.

Instead, it is better to represent your expressions symbolically with SymPy, then use sympy.lambdify to convert them to numerical functions. So for instance, your initFuncts could be (by the way, if your dict keys are just 0 and 1, why not just use a list?) could be

import sympy as sym
x = sym.symbols('x')
initFuncts = [(sym.exp(-x**2/2) + 2*x*sym.exp(-x**2/2))**2, x**2]

(I've also cleaned up the spaces, and used rational numbers instead of floats).

Then you could convert this to a numpy function later with lambdify. For example,

f = lambdify(x, initFuncts[0], 'numpy')

creates a function f which evaluates the first expression as a NumPy function.

\$\endgroup\$
  • \$\begingroup\$ How would one go about converting a raw expression from the user to the sympy expression needed in the lists/dictionaries? I used dictionaries so that eventually the user can input names which can be displayed in the dropdown as well. Thanks for the insight though, lambdify certainly helps! \$\endgroup\$ – logical123 Apr 2 '17 at 20:44
  • \$\begingroup\$ You can use sympy.sympify to convert a string into a sympy expression. \$\endgroup\$ – asmeurer Apr 2 '17 at 21:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.