TP3 - Tranformée de Fourier¶
I) Théorème d'échantillonage¶
Pour illustrer comment échantillonner un signal continue, on va considérer le signal à valeurs complexes suivant
$$ f_a(t) = e^{-\pi (t-2)^2}e^{2\pi i a(t-2)} = h_a(t-2)\qquad a = 1. $$
Ce signal a pour fréquence centrale $a=1$ et, comme on a un décalage, sa transformée de Fourier est simplement
$$ \mathcal{F}f_a(s) = \mathcal{F}h_a(s) e^{2 \pi i b s}\qquad b=-2, $$
où
$$ h_a(t) = e^{-\pi t^2}e^{2\pi i a t}. $$
Comme
$$ \mathcal{F}h_a(s) = e^{-\pi(s-a)^2}, $$
on a au final
$$ \mathcal{F}f_a(s) = e^{-\pi(s-a)^2}e^{2 \pi i b s}. $$
1) Illustrer la fonction $f_a$¶
Importons les librairies dont nous aurons besoin pour faire des graphiques et du calcul numérique.
import matplotlib.pyplot as plt
import numpy as np
Définissons la fonction $f_a$.
a = 1.
f = lambda t: np.exp(- np.pi * (t-2)**2) * np.exp(2*np.pi*1j*a*(t-2))
puis une grille de temps.
T = 4.
t = np.linspace(0, 4, 200)
La partie réelle du signal est donnée par
plt.plot(t,np.real(f(t)))
plt.show()
et la partie imaginaire par
plt.plot(t,np.imag(f(t)))
plt.show()
2) Créer une discrétisation $(t_n)$ de l'intervalle de temps $[0,4]$ de pas de temps $1/(2B)$ (noté t_grid
), où $B$ est un paramètre avec lequel on jouera. Illustrer l'échantillonnage de $f_a$ sur cette grille.¶
On va prendre $B$ comme étant 2 fois la fréquence centrale pour définir notre grille de temps.
B = 2*a
dt = 1/(2*B)
t_grid = np.arange(0, 4, dt)
Evaluons $f_a$ sur la grille t_grid
f_discrete = f(t_grid)
Illustrons le signal $f_a$ et ses points d'échantillonnage
plt.plot(t_grid, np.real(f_discrete), "o")
plt.plot(t, np.real(f(t)))
plt.show()
plt.plot(t_grid, np.imag(f_discrete), "o")
plt.plot(t, np.imag(f(t)))
plt.show()
Définissons la fonction $f_{sample}$.
f_sample = lambda t: np.sum( f_discrete * np.sinc( (t-t_grid)/dt ) )
et évaluons la sur la grille fine t
définie plus haut pour la comparer à $f_a$.
f_reconstruceted = np.array([f_sample(t) for t in t])
plt.plot(t, np.real(f_reconstruceted))
plt.plot(t, np.real(f(t)))
plt.show()
plt.plot(t, np.imag(f_reconstruceted))
plt.plot(t, np.imag(f(t)))
plt.show()
On peut voir que la reconstruction est très bonne. Elle devient moins bonne si on prend $B$ plus petit.
4) Tracer la transformée de Fourier de $f_a$ et tracer les droites verticales d'abscisse $B$. Que peut-on remarquer, et est ce en accord avec l'hypothèse principale du théorème d'échantillonnage?¶
Evaluons la transformée de Fourier de $f_a$ sur une grille fine.
b = -2
s = np.linspace(-1, 2.5, 100)
TF_fa = np.exp(-np.pi*(s-a)**2) * np.exp(2*np.pi*1j*b*s)
Sa partie réelle est donnée par
plt.plot(s, np.real(TF_fa), label='TF')
plt.vlines(0, -0.5, 0.5, label=' freq 0', color='green')
plt.vlines(B, -0.5, 0.5, label=' freq B', color='orange')
plt.legend()
plt.show()
et sa partie imaginaire par
plt.plot(s, np.imag(TF_fa), label='TF')
plt.vlines(0, -0.5, 0.5, label=' freq 0', color='green')
plt.vlines(B, -0.5, 0.5, label=' freq B', color='orange')
plt.legend()
plt.show()
Tout d'abord on n'est pas dans les conditions standards du théorème d'échantillonnage car $\mathcal{F}f_a$ n'est pas limité en bande de fréquence (band limited). Cependant, la fréquence centrale est donnée par $a$ dans le terme $e^{2\pi i a t}$. Le théorème d'échantillonnage nous dirait d'échantillonner à $2a = 2$ (2 fois la fréquence max). Avec ce choix, on observe une mauvaise reconstruction du signal. En un sens cette observation est normale car dans notre cas $a$ n'est pas la fréquence maximale portée par le signal à cause de la gaussienne. Le choix $B=2a$ correspond quasiment à une fréquence maximale et donne une reconstruction quasi partfaite du signal original.
Définissons une grille de fréquence couvrant $[0,2B]$ de pas de temps $1/T$ et une fonction correspondant à la TFD qu'on va évaluer sur la grille.
s = np.arange(0, 2*B, 1/T)
TFD_f = lambda s: np.sum( f_discrete * np.exp(-2*np.pi*1j*s*t_grid) )
TFD_f_discretise = np.array([TFD_f(s) for s in s])
TFD_f_discretise
array([ 1.72852192e-01+1.11022302e-16j, -6.83282922e-01+0.00000000e+00j, 1.82374885e+00+2.22044605e-16j, -3.28690355e+00-1.38777878e-15j, 3.99999626e+00+2.24987512e-15j, -3.28690355e+00-1.88737914e-15j, 1.82374885e+00+1.38777878e-15j, -6.83282922e-01-5.55111512e-16j, 1.72852192e-01+1.11022302e-16j, -2.95305664e-02+5.55111512e-16j, 3.40245297e-03-5.55111512e-16j, -2.69009259e-04-2.77555756e-17j, 2.46532238e-05+1.60383061e-15j, -2.69009259e-04-1.66533454e-16j, 3.40245297e-03+1.11022302e-16j, -2.95305664e-02-1.11022302e-16j])
Comparons la tranformée de Fourier théorique et ce qu'on obtient avec la TFD. Pour la partie réelle on a
TF_fa = np.exp(-np.pi*(s-a)**2) * np.exp(2*np.pi*1j*b*s)
plt.plot(s, np.real(TFD_f_discretise)*dt, "o")
plt.plot(s, np.real(TF_fa) ,"o")
plt.ylim(-1.5, 1.5)
plt.show()
et pour la partie imaginaire
plt.plot(s, np.imag(TFD_f_discretise)*dt, "o")
plt.plot(s, np.imag(TF_fa) ,"o")
plt.ylim(-1.5, 1.5)
plt.show()
On remarquera qu'on a besoin de multiplier par $dt$ pour comparer avec la formulation théorique de la transformée de Fourier.¶
6) Effectuer le même calcul sous forme de produit matriciel (on evitera de faire une boucle for
).¶
Le calcul de la DFT fait intervenir les termes
$$ e^{-2 \pi i n m / N} = (e^{-2 \pi i/ N})^{n m} = \omega^{-nm}$$
qui forment la matrice $\underline{\mathcal{F}}$. On va donc créer la matrice des indices $nm$ et l'évaluer par la fonction définie par l'exponentielle complexe.
N = len(t_grid)
m = np.arange(N).reshape((1,N))
n = m.reshape((N, 1))
(n*m).shape
(16, 16)
On applique cette matrice à l'exponentielle complexe.
M = np.exp(-2 * np.pi * 1j * n * m / N)
La DFT est alors donnée par le produit matrice/vecteur $Mf_{discrete}$
TF_fa_mat = np.dot(M, f_discrete)
TF_fa_mat
array([ 1.72852192e-01+3.08066025e-17j, -6.83282922e-01-6.88252096e-17j, 1.82374885e+00+2.82255477e-16j, -3.28690355e+00-1.48981385e-15j, 3.99999626e+00+2.24987512e-15j, -3.28690355e+00-1.86191077e-15j, 1.82374885e+00+1.31241985e-15j, -6.83282922e-01-5.19964975e-16j, 1.72852192e-01+1.38539704e-16j, -2.95305664e-02+5.66374091e-16j, 3.40245297e-03-5.11594956e-16j, -2.69009259e-04+2.01875289e-17j, 2.46532238e-05+1.60383061e-15j, -2.69009259e-04-2.68258218e-16j, 3.40245297e-03+1.85767674e-16j, -2.95305664e-02-1.87189095e-17j])
plt.plot(s, np.real(TF_fa_mat)*dt, "o")
plt.plot(s, np.real(TF_fa), "o")
plt.ylim(-1.5, 1.5)
plt.show()
plt.plot(s, np.imag(TF_fa_mat)*dt, "o")
plt.plot(s, np.imag(TF_fa), "o")
plt.ylim(-1.5, 1.5)
plt.show()
7) Effectuer le même calcul à l'aide de la function fft
du module numpy.fft
.¶
FFT correspond à Fast Fourier Transform et est un algorithme pour calculer le transformée de Fourier discrète.
from numpy.fft import fft
TFD_f_discretise_np = fft(f_discrete)
TFD_f_discretise_np
array([ 1.72852192e-01+1.70830506e-21j, -6.83282922e-01+1.70830506e-21j, 1.82374885e+00+1.70830506e-21j, -3.28690355e+00+1.70830506e-21j, 3.99999626e+00+1.70830506e-21j, -3.28690355e+00+1.70830506e-21j, 1.82374885e+00+1.70830506e-21j, -6.83282922e-01+1.70830506e-21j, 1.72852192e-01+1.70830506e-21j, -2.95305664e-02+1.70830506e-21j, 3.40245297e-03+1.70830506e-21j, -2.69009259e-04+1.70830506e-21j, 2.46532238e-05+1.70830506e-21j, -2.69009259e-04+1.70830506e-21j, 3.40245297e-03+1.70830506e-21j, -2.95305664e-02+1.70830506e-21j])
plt.plot(s, np.real(TFD_f_discretise_np)*dt, "o")
plt.plot(s, np.real(TF_fa) ,"o")
plt.ylim(-1.5,1.5)
plt.show()
plt.plot(s, np.imag(TFD_f_discretise_np)*dt, "o")
plt.plot(s, np.imag(TF_fa) ,"o")
plt.ylim(-1.5,1.5)
plt.show()
8) Quel est la méthode la plus efficace en terme de temps calcul pour calculer la DFT? On pourra ecrire chacune des méthodes sous forme de fonction, et évaluer le temps de calcul de chacune des méthodes sur des entrées (signaux discrétisés tirés aléatoirement).¶
Les fonctions dont on va évaluer le temps d'execution sont les suivantes:
def TFD_1(x, t_grid):
return np.array([np.sum( x * np.exp(-2*np.pi*1j*s*t_grid)) for s in s])
def TFD_2(x, M):
m = np.arange(N).reshape((1,N))
n = m.reshape((N, 1))
M = np.exp(-2 * np.pi * 1j * n * m / N)
return np.dot(M, x)
def TFD_3(x):
return fft(x)
Pour mesurer le temps d'execution on va utiliser la librairie time
.
import time
On va mesurer le temps qu'il faut pour chaque fonction de faire 100000 évaluations, comprenant la génération aléatoire des signaux discérisés (mais qui est la même pour chacune des fonctions).
N_repeat = 100000
time_exec = 0.
for j in range(N_repeat):
x = np.random.rand(N)
start_time = time.perf_counter()
TFD_1(x, t_grid)
stop_time = time.perf_counter()
time_exec += stop_time - start_time
time_exec
10.62068264796335
N_repeat = 100000
time_exec = 0.
for j in range(N_repeat):
x = np.random.rand(N)
start_time = time.perf_counter()
TFD_2(x, M)
stop_time = time.perf_counter()
time_exec += stop_time - start_time
time_exec
1.8500823179479085
N_repeat = 100000
time_exec = 0.
for j in range(N_repeat):
x = np.random.rand(N)
start_time = time.perf_counter()
TFD_3(x)
stop_time = time.perf_counter()
time_exec += stop_time - start_time
time_exec
0.19817016194065218
Sans surprise la fonction fft
de numpy est la plus performant pour calculer la DFT. Cependant la formulation matricielle, évitant la boucle for
explicite de la première méthode améliore déjà grandement les choses.