1""" 2=============== 3Fourier Demo WX 4=============== 5 6""" 7 8import numpy as np 9 10import wx 11from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas 12from matplotlib.figure import Figure 13 14 15class Knob(object): 16 """ 17 Knob - simple class with a "setKnob" method. 18 A Knob instance is attached to a Param instance, e.g., param.attach(knob) 19 Base class is for documentation purposes. 20 """ 21 22 def setKnob(self, value): 23 pass 24 25 26class Param(object): 27 """ 28 The idea of the "Param" class is that some parameter in the GUI may have 29 several knobs that both control it and reflect the parameter's state, e.g. 30 a slider, text, and dragging can all change the value of the frequency in 31 the waveform of this example. 32 The class allows a cleaner way to update/"feedback" to the other knobs when 33 one is being changed. Also, this class handles min/max constraints for all 34 the knobs. 35 Idea - knob list - in "set" method, knob object is passed as well 36 - the other knobs in the knob list have a "set" method which gets 37 called for the others. 38 """ 39 40 def __init__(self, initialValue=None, minimum=0., maximum=1.): 41 self.minimum = minimum 42 self.maximum = maximum 43 if initialValue != self.constrain(initialValue): 44 raise ValueError('illegal initial value') 45 self.value = initialValue 46 self.knobs = [] 47 48 def attach(self, knob): 49 self.knobs += [knob] 50 51 def set(self, value, knob=None): 52 self.value = value 53 self.value = self.constrain(value) 54 for feedbackKnob in self.knobs: 55 if feedbackKnob != knob: 56 feedbackKnob.setKnob(self.value) 57 return self.value 58 59 def constrain(self, value): 60 if value <= self.minimum: 61 value = self.minimum 62 if value >= self.maximum: 63 value = self.maximum 64 return value 65 66 67class SliderGroup(Knob): 68 def __init__(self, parent, label, param): 69 self.sliderLabel = wx.StaticText(parent, label=label) 70 self.sliderText = wx.TextCtrl(parent, -1, style=wx.TE_PROCESS_ENTER) 71 self.slider = wx.Slider(parent, -1) 72 # self.slider.SetMax(param.maximum*1000) 73 self.slider.SetRange(0, param.maximum * 1000) 74 self.setKnob(param.value) 75 76 sizer = wx.BoxSizer(wx.HORIZONTAL) 77 sizer.Add(self.sliderLabel, 0, 78 wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, 79 border=2) 80 sizer.Add(self.sliderText, 0, 81 wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, 82 border=2) 83 sizer.Add(self.slider, 1, wx.EXPAND) 84 self.sizer = sizer 85 86 self.slider.Bind(wx.EVT_SLIDER, self.sliderHandler) 87 self.sliderText.Bind(wx.EVT_TEXT_ENTER, self.sliderTextHandler) 88 89 self.param = param 90 self.param.attach(self) 91 92 def sliderHandler(self, evt): 93 value = evt.GetInt() / 1000. 94 self.param.set(value) 95 96 def sliderTextHandler(self, evt): 97 value = float(self.sliderText.GetValue()) 98 self.param.set(value) 99 100 def setKnob(self, value): 101 self.sliderText.SetValue('%g' % value) 102 self.slider.SetValue(value * 1000) 103 104 105class FourierDemoFrame(wx.Frame): 106 def __init__(self, *args, **kwargs): 107 wx.Frame.__init__(self, *args, **kwargs) 108 panel = wx.Panel(self) 109 110 # create the GUI elements 111 self.createCanvas(panel) 112 self.createSliders(panel) 113 114 # place them in a sizer for the Layout 115 sizer = wx.BoxSizer(wx.VERTICAL) 116 sizer.Add(self.canvas, 1, wx.EXPAND) 117 sizer.Add(self.frequencySliderGroup.sizer, 0, 118 wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) 119 sizer.Add(self.amplitudeSliderGroup.sizer, 0, 120 wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) 121 panel.SetSizer(sizer) 122 123 def createCanvas(self, parent): 124 self.lines = [] 125 self.figure = Figure() 126 self.canvas = FigureCanvas(parent, -1, self.figure) 127 self.canvas.callbacks.connect('button_press_event', self.mouseDown) 128 self.canvas.callbacks.connect('motion_notify_event', self.mouseMotion) 129 self.canvas.callbacks.connect('button_release_event', self.mouseUp) 130 self.state = '' 131 self.mouseInfo = (None, None, None, None) 132 self.f0 = Param(2., minimum=0., maximum=6.) 133 self.A = Param(1., minimum=0.01, maximum=2.) 134 self.createPlots() 135 136 # Not sure I like having two params attached to the same Knob, 137 # but that is what we have here... it works but feels kludgy - 138 # although maybe it's not too bad since the knob changes both params 139 # at the same time (both f0 and A are affected during a drag) 140 self.f0.attach(self) 141 self.A.attach(self) 142 143 def createSliders(self, panel): 144 self.frequencySliderGroup = SliderGroup( 145 panel, 146 label='Frequency f0:', 147 param=self.f0) 148 self.amplitudeSliderGroup = SliderGroup(panel, label=' Amplitude a:', 149 param=self.A) 150 151 def mouseDown(self, evt): 152 if self.lines[0].contains(evt)[0]: 153 self.state = 'frequency' 154 elif self.lines[1].contains(evt)[0]: 155 self.state = 'time' 156 else: 157 self.state = '' 158 self.mouseInfo = (evt.xdata, evt.ydata, 159 max(self.f0.value, .1), 160 self.A.value) 161 162 def mouseMotion(self, evt): 163 if self.state == '': 164 return 165 x, y = evt.xdata, evt.ydata 166 if x is None: # outside the axes 167 return 168 x0, y0, f0Init, AInit = self.mouseInfo 169 self.A.set(AInit + (AInit * (y - y0) / y0), self) 170 if self.state == 'frequency': 171 self.f0.set(f0Init + (f0Init * (x - x0) / x0)) 172 elif self.state == 'time': 173 if (x - x0) / x0 != -1.: 174 self.f0.set(1. / (1. / f0Init + (1. / f0Init * (x - x0) / x0))) 175 176 def mouseUp(self, evt): 177 self.state = '' 178 179 def createPlots(self): 180 # This method creates the subplots, waveforms and labels. 181 # Later, when the waveforms or sliders are dragged, only the 182 # waveform data will be updated (not here, but below in setKnob). 183 if not hasattr(self, 'subplot1'): 184 self.subplot1, self.subplot2 = self.figure.subplots(2) 185 x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value) 186 color = (1., 0., 0.) 187 self.lines += self.subplot1.plot(x1, y1, color=color, linewidth=2) 188 self.lines += self.subplot2.plot(x2, y2, color=color, linewidth=2) 189 # Set some plot attributes 190 self.subplot1.set_title( 191 "Click and drag waveforms to change frequency and amplitude", 192 fontsize=12) 193 self.subplot1.set_ylabel("Frequency Domain Waveform X(f)", fontsize=8) 194 self.subplot1.set_xlabel("frequency f", fontsize=8) 195 self.subplot2.set_ylabel("Time Domain Waveform x(t)", fontsize=8) 196 self.subplot2.set_xlabel("time t", fontsize=8) 197 self.subplot1.set_xlim([-6, 6]) 198 self.subplot1.set_ylim([0, 1]) 199 self.subplot2.set_xlim([-2, 2]) 200 self.subplot2.set_ylim([-2, 2]) 201 self.subplot1.text(0.05, .95, 202 r'$X(f) = \mathcal{F}\{x(t)\}$', 203 verticalalignment='top', 204 transform=self.subplot1.transAxes) 205 self.subplot2.text(0.05, .95, 206 r'$x(t) = a \cdot \cos(2\pi f_0 t) e^{-\pi t^2}$', 207 verticalalignment='top', 208 transform=self.subplot2.transAxes) 209 210 def compute(self, f0, A): 211 f = np.arange(-6., 6., 0.02) 212 t = np.arange(-2., 2., 0.01) 213 x = A * np.cos(2 * np.pi * f0 * t) * np.exp(-np.pi * t ** 2) 214 X = A / 2 * \ 215 (np.exp(-np.pi * (f - f0) ** 2) + np.exp(-np.pi * (f + f0) ** 2)) 216 return f, X, t, x 217 218 def setKnob(self, value): 219 # Note, we ignore value arg here and just go by state of the params 220 x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value) 221 # update the data of the two waveforms 222 self.lines[0].set(xdata=x1, ydata=y1) 223 self.lines[1].set(xdata=x2, ydata=y2) 224 # make the canvas draw its contents again with the new data 225 self.canvas.draw() 226 227 228class App(wx.App): 229 def OnInit(self): 230 self.frame1 = FourierDemoFrame(parent=None, title="Fourier Demo", 231 size=(640, 480)) 232 self.frame1.Show() 233 return True 234 235app = App() 236app.MainLoop() 237