예제 #1
0
class SandPendulumGUI:
    def __init__(self, window, l_x=1, l_y=.64, t_max=5, d_time=.03):
        self.window = window  # store handle to window
        window.title("Sand pendulum Lissajous patterns")

        # initialise pendulum parameters
        self.x0 = None
        self.y0 = None
        self.v_x0 = None
        self.v_y0 = None
        self.l_x = l_x
        self.l_y = l_y
        self.t_max = t_max
        self.d_time = d_time
        self.speed_multiplier = 4  # relating drag distance to initial velocity
        self.active = False
        self.predict_path = BooleanVar()
        self.predict_path.initialize(True)
        self.show_ratio = BooleanVar()
        self.show_ratio.initialize(False)

        # create the mpl Figure instance on which to plot
        fig = Figure(figsize=(5, 5))
        ax = fig.add_subplot(111)
        self.fig = fig  # store these handles
        self.ax = ax
        ax.set_aspect("equal")  # equal aspect ratio
        for spine in ax.spines.values():  # move spines to origin
            spine.set_position(("data", 0))
        lim = -1, 1  # set axes limits
        plt.setp(
            ax,
            xlim=lim,
            ylim=lim,
            xticks=lim,
            yticks=lim,
        )

        # define the canvas to house the mpl Figure
        canvas = FigureCanvasTkAgg(fig, master=self.window)
        canvas.get_tk_widget().pack()
        canvas.draw()
        # set the default mathtext font
        plt.rcParams['mathtext.fontset'] = 'stix'

        # define matplotlib handlers for mouse events
        canvas.mpl_connect('button_press_event', self.canvasClick)
        canvas.mpl_connect('button_release_event', self.canvasRelease)

        # define entry fields for parameters
        def makeEntry(parent,
                      caption,
                      default,
                      side=None,
                      width=None,
                      **options):
            Label(parent, text=caption).pack(side=side)
            entry = Entry(parent, **options)
            if width is not None:
                entry.config(width=width)
            entry.pack(side=side)
            entry.insert(0, default)
            return entry

        self.l_x_entry = makeEntry(window, "Total length: L (m)", self.l_x)
        self.l_y_entry = makeEntry(window, "Length of pendulum 2: l (m)",
                                   self.l_y)
        self.t_max_entry = makeEntry(window, "Time to simulate: t_max (s)",
                                     self.t_max)
        self.d_time_entry = makeEntry(window, "Time increment: d_time (s)",
                                      self.d_time)
        self.speed_multiplier_entry = makeEntry(window,
                                                "Throw speed multiplier (-)",
                                                self.speed_multiplier)

        # define checkbutton for predict path yes/no
        cb = Checkbutton(window,
                         text="predict path",
                         onvalue=True,
                         offvalue=False,
                         command=lambda: self.toggle(self.predict_path))
        cb.pack()
        if self.predict_path.get():
            cb.select()  # set to checked by default
        self.predict_path_button = cb

        # define checkbutton for displaying mathtext yes/no
        cb = Checkbutton(window,
                         text="show ratio",
                         onvalue=True,
                         offvalue=False,
                         command=lambda: self.toggle(self.show_ratio))
        cb.pack()
        if self.show_ratio.get():
            cb.select()  # set to checked by default
        self.show_ratio_button = cb

        # define clear canvas button
        self.clear_button = Button(window,
                                   text="Clear",
                                   command=self.clear_axes)
        self.clear_button.pack()  #side=LEFT)

        # define save figure button
        self.save_button = Button(window,
                                  text="Save figure",
                                  command=self.save_figure)
        self.save_button.pack()  #side=LEFT)

        # define exit button
        self.close_button = Button(window, text="Close", command=window.quit)
        self.close_button.pack()  #side=RIGHT)

    def toggle(self, var):
        var.set(not var.get())

    def canvasClick(self, event):
        if event.button != 1:  # ignore mouse clicks that aren't button 1
            return None
        self.x0 = event.xdata
        self.y0 = event.ydata
        self.ax.plot(event.xdata, event.ydata, ".k")
        self.fig.canvas.draw_idle()

    def canvasRelease(self, event):
        self.update_params()  # update parameters from entry fields
        self.v_x0 = event.xdata - self.x0
        self.v_y0 = event.ydata - self.y0
        self.ax.plot(event.xdata, event.ydata, ".k")
        if (self.v_x0 != 0) or (self.v_y0 != 0):
            self.ax.arrow(self.x0,
                          self.y0,
                          self.v_x0,
                          self.v_y0,
                          zorder=10,
                          width=0.02,
                          lw=0,
                          color="firebrick",
                          length_includes_head=True)
        self.fig.canvas.draw_idle()
        self.v_x0 = self.speed_multiplier * self.v_x0
        self.v_y0 = self.speed_multiplier * self.v_y0
        # recalculate coefficients
        temp = lissajous_constants(self.x0, self.v_x0, self.y0, self.v_y0,
                                   self.l_x, self.l_y)

        if self.show_ratio.get() is True:
            ratio = Fraction(str(np.sqrt(self.l_y / self.l_x)))
            s = r"$\sqrt{{l\,/\,L}}=\frac{{{}}}{{{}}}={}$".format(
                ratio.numerator, ratio.denominator,
                ratio.numerator / ratio.denominator)
            self.ax.text(-1,
                         1,
                         s,
                         ha="left",
                         va="top",
                         fontsize=20,
                         color=".5",
                         zorder=999,
                         alpha=1)

        # plot path
        self.plot_lissajous(*temp, self.l_x, self.l_y, self.t_max, self.d_time)

    def update_params(self):
        # update the parameters from the entry fields
        self.l_x = float(self.l_x_entry.get())
        self.l_y = float(self.l_y_entry.get())
        self.t_max = float(self.t_max_entry.get())
        self.d_time = float(self.d_time_entry.get())
        self.speed_multiplier = float(self.speed_multiplier_entry.get())

    def clear_axes(self):
        for artist in self.ax.lines + self.ax.collections + self.ax.artists + self.ax.texts:
            artist.remove()  # remove all artists (inverse Cass Art)
        self.fig.canvas.draw_idle()  # update the axes

    def save_figure(self):
        self.fig.savefig("output.png")

    def plot_lissajous(self, A_x, A_y, w_x, w_y, d_x, d_y, l_x, l_y, t_max,
                       d_time):
        self.active = True  # set the active flag so no other events are logged
        # generate the x, y data
        xs, ys = lissajous_range(A_x, A_y, w_x, w_y, d_x, d_y, l_x, l_y, t_max,
                                 d_time)

        colours = plt.cm.inferno_r(np.linspace(.1, 1, len(xs)))
        if self.predict_path.get() is True:
            # static plot
            for ii, c in zip(range(1, len(xs)), colours):
                self.ax.plot((xs[ii - 1], xs[ii]), (ys[ii - 1], ys[ii]),
                             "-",
                             c=c)

        # animated plot
        def update_plot():
            nonlocal ii, traj
            if ii > len(xs):
                return None  # exit the loop
            c = colours[:ii]
            traj.set_color(c)
            traj.set_offsets(np.vstack([xs[:ii], ys[:ii]]).T)
            self.fig.canvas.draw_idle()
            ii += 1
            self.window.after(int(d_time * 1000), update_plot)

        traj = self.ax.scatter([], [], cmap=plt.cm.inferno_r)
        ii = 0
        update_plot()
        self.active = False  # reset the active flag to false when done