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