{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n# Fourier Demo WX\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import wx\n\nimport numpy as np\n\nfrom matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas\nfrom matplotlib.figure import Figure\n\n\nclass Knob:\n \"\"\"\n Knob - simple class with a \"setKnob\" method.\n A Knob instance is attached to a Param instance, e.g., param.attach(knob)\n Base class is for documentation purposes.\n \"\"\"\n\n def setKnob(self, value):\n pass\n\n\nclass Param:\n \"\"\"\n The idea of the \"Param\" class is that some parameter in the GUI may have\n several knobs that both control it and reflect the parameter's state, e.g.\n a slider, text, and dragging can all change the value of the frequency in\n the waveform of this example.\n The class allows a cleaner way to update/\"feedback\" to the other knobs when\n one is being changed. Also, this class handles min/max constraints for all\n the knobs.\n Idea - knob list - in \"set\" method, knob object is passed as well\n - the other knobs in the knob list have a \"set\" method which gets\n called for the others.\n \"\"\"\n\n def __init__(self, initialValue=None, minimum=0., maximum=1.):\n self.minimum = minimum\n self.maximum = maximum\n if initialValue != self.constrain(initialValue):\n raise ValueError('illegal initial value')\n self.value = initialValue\n self.knobs = []\n\n def attach(self, knob):\n self.knobs += [knob]\n\n def set(self, value, knob=None):\n self.value = value\n self.value = self.constrain(value)\n for feedbackKnob in self.knobs:\n if feedbackKnob != knob:\n feedbackKnob.setKnob(self.value)\n return self.value\n\n def constrain(self, value):\n if value <= self.minimum:\n value = self.minimum\n if value >= self.maximum:\n value = self.maximum\n return value\n\n\nclass SliderGroup(Knob):\n def __init__(self, parent, label, param):\n self.sliderLabel = wx.StaticText(parent, label=label)\n self.sliderText = wx.TextCtrl(parent, -1, style=wx.TE_PROCESS_ENTER)\n self.slider = wx.Slider(parent, -1)\n # self.slider.SetMax(param.maximum*1000)\n self.slider.SetRange(0, int(param.maximum * 1000))\n self.setKnob(param.value)\n\n sizer = wx.BoxSizer(wx.HORIZONTAL)\n sizer.Add(self.sliderLabel, 0,\n wx.EXPAND | wx.ALL,\n border=2)\n sizer.Add(self.sliderText, 0,\n wx.EXPAND | wx.ALL,\n border=2)\n sizer.Add(self.slider, 1, wx.EXPAND)\n self.sizer = sizer\n\n self.slider.Bind(wx.EVT_SLIDER, self.sliderHandler)\n self.sliderText.Bind(wx.EVT_TEXT_ENTER, self.sliderTextHandler)\n\n self.param = param\n self.param.attach(self)\n\n def sliderHandler(self, event):\n value = event.GetInt() / 1000.\n self.param.set(value)\n\n def sliderTextHandler(self, event):\n value = float(self.sliderText.GetValue())\n self.param.set(value)\n\n def setKnob(self, value):\n self.sliderText.SetValue(f'{value:g}')\n self.slider.SetValue(int(value * 1000))\n\n\nclass FourierDemoFrame(wx.Frame):\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n panel = wx.Panel(self)\n\n # create the GUI elements\n self.createCanvas(panel)\n self.createSliders(panel)\n\n # place them in a sizer for the Layout\n sizer = wx.BoxSizer(wx.VERTICAL)\n sizer.Add(self.canvas, 1, wx.EXPAND)\n sizer.Add(self.frequencySliderGroup.sizer, 0,\n wx.EXPAND | wx.ALL, border=5)\n sizer.Add(self.amplitudeSliderGroup.sizer, 0,\n wx.EXPAND | wx.ALL, border=5)\n panel.SetSizer(sizer)\n\n def createCanvas(self, parent):\n self.lines = []\n self.figure = Figure()\n self.canvas = FigureCanvas(parent, -1, self.figure)\n self.canvas.callbacks.connect('button_press_event', self.mouseDown)\n self.canvas.callbacks.connect('motion_notify_event', self.mouseMotion)\n self.canvas.callbacks.connect('button_release_event', self.mouseUp)\n self.state = ''\n self.mouseInfo = (None, None, None, None)\n self.f0 = Param(2., minimum=0., maximum=6.)\n self.A = Param(1., minimum=0.01, maximum=2.)\n self.createPlots()\n\n # Not sure I like having two params attached to the same Knob,\n # but that is what we have here... it works but feels kludgy -\n # although maybe it's not too bad since the knob changes both params\n # at the same time (both f0 and A are affected during a drag)\n self.f0.attach(self)\n self.A.attach(self)\n\n def createSliders(self, panel):\n self.frequencySliderGroup = SliderGroup(\n panel,\n label='Frequency f0:',\n param=self.f0)\n self.amplitudeSliderGroup = SliderGroup(panel, label=' Amplitude a:',\n param=self.A)\n\n def mouseDown(self, event):\n if self.lines[0].contains(event)[0]:\n self.state = 'frequency'\n elif self.lines[1].contains(event)[0]:\n self.state = 'time'\n else:\n self.state = ''\n self.mouseInfo = (event.xdata, event.ydata,\n max(self.f0.value, .1),\n self.A.value)\n\n def mouseMotion(self, event):\n if self.state == '':\n return\n x, y = event.xdata, event.ydata\n if x is None: # outside the Axes\n return\n x0, y0, f0Init, AInit = self.mouseInfo\n self.A.set(AInit + (AInit * (y - y0) / y0), self)\n if self.state == 'frequency':\n self.f0.set(f0Init + (f0Init * (x - x0) / x0))\n elif self.state == 'time':\n if (x - x0) / x0 != -1.:\n self.f0.set(1. / (1. / f0Init + (1. / f0Init * (x - x0) / x0)))\n\n def mouseUp(self, event):\n self.state = ''\n\n def createPlots(self):\n # This method creates the subplots, waveforms and labels.\n # Later, when the waveforms or sliders are dragged, only the\n # waveform data will be updated (not here, but below in setKnob).\n self.subplot1, self.subplot2 = self.figure.subplots(2)\n x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)\n color = (1., 0., 0.)\n self.lines += self.subplot1.plot(x1, y1, color=color, linewidth=2)\n self.lines += self.subplot2.plot(x2, y2, color=color, linewidth=2)\n # Set some plot attributes\n self.subplot1.set_title(\n \"Click and drag waveforms to change frequency and amplitude\",\n fontsize=12)\n self.subplot1.set_ylabel(\"Frequency Domain Waveform X(f)\", fontsize=8)\n self.subplot1.set_xlabel(\"frequency f\", fontsize=8)\n self.subplot2.set_ylabel(\"Time Domain Waveform x(t)\", fontsize=8)\n self.subplot2.set_xlabel(\"time t\", fontsize=8)\n self.subplot1.set_xlim([-6, 6])\n self.subplot1.set_ylim([0, 1])\n self.subplot2.set_xlim([-2, 2])\n self.subplot2.set_ylim([-2, 2])\n self.subplot1.text(0.05, .95,\n r'$X(f) = \\mathcal{F}\\{x(t)\\}$',\n verticalalignment='top',\n transform=self.subplot1.transAxes)\n self.subplot2.text(0.05, .95,\n r'$x(t) = a \\cdot \\cos(2\\pi f_0 t) e^{-\\pi t^2}$',\n verticalalignment='top',\n transform=self.subplot2.transAxes)\n\n def compute(self, f0, A):\n f = np.arange(-6., 6., 0.02)\n t = np.arange(-2., 2., 0.01)\n x = A * np.cos(2 * np.pi * f0 * t) * np.exp(-np.pi * t ** 2)\n X = A / 2 * \\\n (np.exp(-np.pi * (f - f0) ** 2) + np.exp(-np.pi * (f + f0) ** 2))\n return f, X, t, x\n\n def setKnob(self, value):\n # Note, we ignore value arg here and just go by state of the params\n x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)\n # update the data of the two waveforms\n self.lines[0].set(xdata=x1, ydata=y1)\n self.lines[1].set(xdata=x2, ydata=y2)\n # make the canvas draw its contents again with the new data\n self.canvas.draw()\n\n\nclass App(wx.App):\n def OnInit(self):\n self.frame1 = FourierDemoFrame(parent=None, title=\"Fourier Demo\",\n size=(640, 480))\n self.frame1.Show()\n return True\n\n\nif __name__ == \"__main__\":\n app = App()\n app.MainLoop()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 0 }