Créer une visualisation interactive de la forme d'onde de Fourier

PythonPythonBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire, vous allez apprendre à créer une interface graphique utilisateur (GUI) simple à l'aide de Matplotlib en Python. Plus précisément, vous allez créer un démonstrateur de Fourier qui affiche deux formes d'ondes dans les domaines de la fréquence et du temps. Vous pourrez ajuster la fréquence et l'amplitude des formes d'ondes en cliquant et en traînant sur le graphique.

Conseils sur la machine virtuelle (VM)

Une fois le démarrage de la VM terminé, cliquez dans le coin supérieur gauche pour basculer vers l'onglet Carnet de notes pour accéder au carnet Jupyter Notebook pour la pratique.

Parfois, vous devrez peut-être attendre quelques secondes pour que le carnet Jupyter Notebook ait fini de charger. La validation des opérations ne peut pas être automatisée en raison des limites du carnet Jupyter Notebook.

Si vous rencontrez des problèmes pendant l'apprentissage, n'hésitez pas à demander à Labby. Donnez votre feedback après la session, et nous résoudrons rapidement le problème pour vous.

Importation des bibliothèques

La première étape consiste à importer les bibliothèques nécessaires. Nous allons utiliser Matplotlib, wxPython et NumPy. Matplotlib est une bibliothèque de tracé pour Python, wxPython est un kit de développement d'interface graphique (GUI) pour Python, et NumPy est une bibliothèque pour le calcul numérique avec Python.

import wx
import numpy as np
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure

Définition des classes Knob et Param

L'étape suivante consiste à définir les classes Knob et Param. Ces classes seront utilisées pour contrôler la fréquence et l'amplitude des formes d'ondes dans l'interface graphique utilisateur (GUI).

class Knob:
    """
    Knob - simple class with a "setKnob" method.
    A Knob instance is attached to a Param instance, e.g., param.attach(knob)
    Base class is for documentation purposes.
    """

    def setKnob(self, value):
        pass


class Param:
    """
    L'idée de la classe "Param" est que certains paramètres dans l'interface graphique peuvent avoir
    plusieurs boutons de réglage qui le contrôlent et reflètent également l'état du paramètre, par exemple
    un curseur, du texte et le fait de faire glisser peuvent tous modifier la valeur de la fréquence dans
    la forme d'onde de cet exemple.
    Cette classe permet une manière plus propre de mettre à jour / "renvoyer des informations" aux autres boutons de réglage lorsque
    l'un d'entre eux est modifié.  De plus, cette classe gère les contraintes min/max pour tous
    les boutons de réglage.
    Idée - liste de boutons de réglage - dans la méthode "set", l'objet bouton de réglage est également passé
      - les autres boutons de réglage dans la liste des boutons de réglage ont une méthode "set" qui est
        appelée pour les autres.
    """

    def __init__(self, initialValue=None, minimum=0., maximum=1.):
        self.minimum = minimum
        self.maximum = maximum
        if initialValue!= self.constrain(initialValue):
            raise ValueError('illegal initial value')
        self.value = initialValue
        self.knobs = []

    def attach(self, knob):
        self.knobs += [knob]

    def set(self, value, knob=None):
        self.value = value
        self.value = self.constrain(value)
        for feedbackKnob in self.knobs:
            if feedbackKnob!= knob:
                feedbackKnob.setKnob(self.value)
        return self.value

    def constrain(self, value):
        if value <= self.minimum:
            value = self.minimum
        if value >= self.maximum:
            value = self.maximum
        return value

Définition de la classe SliderGroup

La classe SliderGroup créera un curseur et un champ de texte dans l'interface graphique utilisateur (GUI) pour ajuster la fréquence et l'amplitude des formes d'ondes.

class SliderGroup(Knob):
    def __init__(self, parent, label, param):
        self.sliderLabel = wx.StaticText(parent, label=label)
        self.sliderText = wx.TextCtrl(parent, -1, style=wx.TE_PROCESS_ENTER)
        self.slider = wx.Slider(parent, -1)
        self.slider.SetRange(0, int(param.maximum * 1000))
        self.setKnob(param.value)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.sliderLabel, 0,
                  wx.EXPAND | wx.ALL,
                  border=2)
        sizer.Add(self.sliderText, 0,
                  wx.EXPAND | wx.ALL,
                  border=2)
        sizer.Add(self.slider, 1, wx.EXPAND)
        self.sizer = sizer

        self.slider.Bind(wx.EVT_SLIDER, self.sliderHandler)
        self.sliderText.Bind(wx.EVT_TEXT_ENTER, self.sliderTextHandler)

        self.param = param
        self.param.attach(self)

    def sliderHandler(self, event):
        value = event.GetInt() / 1000.
        self.param.set(value)

    def sliderTextHandler(self, event):
        value = float(self.sliderText.GetValue())
        self.param.set(value)

    def setKnob(self, value):
        self.sliderText.SetValue(f'{value:g}')
        self.slider.SetValue(int(value * 1000))

Définition de la classe FourierDemoFrame

La classe FourierDemoFrame créera l'interface graphique utilisateur (GUI) à l'aide de wxPython et Matplotlib.

class FourierDemoFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        panel = wx.Panel(self)

        ## créer les éléments de l'interface graphique
        self.createCanvas(panel)
        self.createSliders(panel)

        ## les placer dans un sizer pour la mise en page
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.canvas, 1, wx.EXPAND)
        sizer.Add(self.frequencySliderGroup.sizer, 0,
                  wx.EXPAND | wx.ALL, border=5)
        sizer.Add(self.amplitudeSliderGroup.sizer, 0,
                  wx.EXPAND | wx.ALL, border=5)
        panel.SetSizer(sizer)

    def createCanvas(self, parent):
        self.lines = []
        self.figure = Figure()
        self.canvas = FigureCanvas(parent, -1, self.figure)
        self.canvas.callbacks.connect('button_press_event', self.mouseDown)
        self.canvas.callbacks.connect('motion_notify_event', self.mouseMotion)
        self.canvas.callbacks.connect('button_release_event', self.mouseUp)
        self.state = ''
        self.mouseInfo = (None, None, None, None)
        self.f0 = Param(2., minimum=0., maximum=6.)
        self.A = Param(1., minimum=0.01, maximum=2.)
        self.createPlots()

        self.f0.attach(self)
        self.A.attach(self)

    def createSliders(self, panel):
        self.frequencySliderGroup = SliderGroup(
            panel,
            label='Fréquence f0 :',
            param=self.f0)
        self.amplitudeSliderGroup = SliderGroup(panel, label=' Amplitude a :',
                                                param=self.A)

    def mouseDown(self, event):
        if self.lines[0].contains(event)[0]:
            self.state = 'fréquence'
        elif self.lines[1].contains(event)[0]:
            self.state = 'temps'
        else:
            self.state = ''
        self.mouseInfo = (event.xdata, event.ydata,
                          max(self.f0.value,.1),
                          self.A.value)

    def mouseMotion(self, event):
        if self.state == '':
            return
        x, y = event.xdata, event.ydata
        if x is None:  ## en dehors des axes
            return
        x0, y0, f0Init, AInit = self.mouseInfo
        self.A.set(AInit + (AInit * (y - y0) / y0), self)
        if self.state == 'fréquence':
            self.f0.set(f0Init + (f0Init * (x - x0) / x0))
        elif self.state == 'temps':
            if (x - x0) / x0!= -1.:
                self.f0.set(1. / (1. / f0Init + (1. / f0Init * (x - x0) / x0)))

    def mouseUp(self, event):
        self.state = ''

    def createPlots(self):
        self.subplot1, self.subplot2 = self.figure.subplots(2)
        x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)
        couleur = (1., 0., 0.)
        self.lines += self.subplot1.plot(x1, y1, couleur=couleur, linewidth=2)
        self.lines += self.subplot2.plot(x2, y2, couleur=couleur, linewidth=2)
        self.subplot1.set_title(
            "Cliquez et faites glisser les formes d'onde pour changer la fréquence et l'amplitude",
            fontsize=12)
        self.subplot1.set_ylabel("Forme d'onde dans le domaine de la fréquence X(f)", fontsize=8)
        self.subplot1.set_xlabel("fréquence f", fontsize=8)
        self.subplot2.set_ylabel("Forme d'onde dans le domaine du temps x(t)", fontsize=8)
        self.subplot2.set_xlabel("temps t", fontsize=8)
        self.subplot1.set_xlim([-6, 6])
        self.subplot1.set_ylim([0, 1])
        self.subplot2.set_xlim([-2, 2])
        self.subplot2.set_ylim([-2, 2])
        self.subplot1.text(0.05,.95,
                           r'$X(f) = \mathcal{F}\{x(t)\}$',
                           verticalalignment='top',
                           transform=self.subplot1.transAxes)
        self.subplot2.text(0.05,.95,
                           r'$x(t) = a \cdot \cos(2\pi f_0 t) e^{-\pi t^2}$',
                           verticalalignment='top',
                           transform=self.subplot2.transAxes)

    def compute(self, f0, A):
        f = np.arange(-6., 6., 0.02)
        t = np.arange(-2., 2., 0.01)
        x = A * np.cos(2 * np.pi * f0 * t) * np.exp(-np.pi * t ** 2)
        X = A / 2 * \
            (np.exp(-np.pi * (f - f0) ** 2) + np.exp(-np.pi * (f + f0) ** 2))
        return f, X, t, x

    def setKnob(self, value):
        x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)
        self.lines[0].set(xdata=x1, ydata=y1)
        self.lines[1].set(xdata=x2, ydata=y2)
        self.canvas.draw()

Définition de la classe App

La classe App créera l'application et affichera l'interface graphique utilisateur (GUI).

class App(wx.App):
    def OnInit(self):
        self.frame1 = FourierDemoFrame(parent=None, title="Fourier Demo",
                                       size=(640, 480))
        self.frame1.Show()
        return True

Exécuter l'application

La dernière étape consiste à exécuter l'application.

if __name__ == "__main__":
    app = App()
    app.MainLoop()

Sommaire

Dans ce laboratoire, vous avez appris à créer une interface graphique utilisateur (GUI) simple à l'aide de Matplotlib en Python. Vous avez créé un démonstrateur de Fourier qui affiche deux formes d'onde dans les domaines de la fréquence et du temps. Vous avez pu ajuster la fréquence et l'amplitude des formes d'onde en cliquant et en faisant glisser sur le tracé.