{
  "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.14.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}