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