Par J.-F. Bercher -- le 12 novembre 2013
Une version pdf de ce texte peut être obtenue ici
Notebook lancé selon : ipython3 notebook --pylab='inline'
Version temporaire.
Ce TP est une introduction, très élémentaire, à l'analyse d'un filtrage, et en particulier aux notions de réponse impulsionnelle, convolution, représentation fréquentielle, fonction de transfert.
Dans ces exercices, on travaillera bien entendu avec des signaux échantillonnés. Les expérimentations seront réalisées sous Python. Ici on a utilisé un Python v. 3.2.
On considère la relation de filtrage décrite par l’équation aux différences suivante : \[y(n)=a y(n-1) + x(n)\] où \(x(n)\) est l’entrée du filtre et \(y(n)\) sa sortie.
from pylab import *
On commence par créer une fonction qui rend une impulsion de Dirac, et on teste le résultat
def dirac(n):
""" dirac(n):
Rend une impulsion de Dirac de longueur n """
d=zeros(n)
d[0]=1
return d
# Représentation
N=100
stem(range(N),dirac(N))
title("Dirac $\delta(n)$")
xlabel("n")
ylim([0, 1.2]) # Pour mieux voir l'impulsion
xlim([-5, 10])
import scipy
from scipy.signal import lfilter
help(lfilter)
En identifiant les paramètres, on voit que filtrer un signal quelconque \(x\) suivant l'équation \(y(n)=a y(n-1)+x(n)\) correspond à utiliser la commande y=lfilter([1],[1, -a],x), où bien entendu x et a ont été initialisés au préalable. Pour obtenir la réponse impulsionnelle, il suffit de mettre en entrée du système une impulsion !
a=0.8
N=100
x=dirac(N)
y=lfilter([1],[1, -a],x)
stem(y),
title("Réponse impulsionnelle pour a={}".format(a)), xlabel("n")
Les premières valeurs sont :
print("Premières valeurs \n y[:6]=" , y[:6])
print("que l'on compare à a**n :\n", a**arange(0,6))
On constate donc que la RI "expérimentale" correspond bien à la RI prévue théoriquement, à savoir \(h(n)=a^n\).
On va le contrôler pour d'autres valeurs de \(a\).
Pour ne pas trop s'embêter, on va définir une fonction qui renvoir la RI, pour deux vecteurs [b] et [a] quelconque décrinat le filtre. Il suffit de calculer la sortie du filtre avec comme entrée un Dirac, sur une longueur spécifiée :
def ri(b,a,n):
u""" Rend la
réponse impulsionnelle de longueur
n (n entier) d'un filtre de coefficients b,a
\n
Attention : b et a _doivent_ être des arrays ou
des listes -- si b=1, entrer b=[1]"""
return lfilter(b,a,dirac(n))
N=25
axe_n=range(N)
a=-0.8
figure()
stem(axe_n,ri([1],[1, -a],N))
title("Réponse impulsionnelle pour a={}".format(a))
xlabel("n")
#
N=200
axe_n=range(N)
a=0.99
figure()
stem(axe_n,ri([1],[1, -a],N))
title("Réponse impulsionnelle pour a={}".format(a))
xlabel("n")
#
a=1.01
figure()
stem(axe_n,ri([1],[1, -a],N))
title("Réponse impulsionnelle pour a={}".format(a))
xlabel("n")
Conclusions :
Pour \(a<0\), la RI qui est théoriquement \(a^n\) est effectivement alternée
pour \(a\) proche de 1, par valeur inférieur, la RI est presque constante
pour \(a>1\), la RI diverge...
# On aura besoin des fonctions fft
from numpy.fft import fft, ifft
# Calcul d'une réponse impusionnelle
a=0.8
h=ri([1],[1, -a],300)
# Calcul de la réponse en fréquence
M=1000
Fe=32
H=fftshift(fft(h,M)) # On utilise ffshift pour centrer la représentation sur 0
f=arange(M)/M*Fe -Fe/2 # Définition de l'axe des fréquences, entre -Fe/2 et Fe/2, sur M points
fig=figure(4) # Et affichage
subplot(2,1,1)
plot(f,abs(H),label=u"Réponse en fréquence")
xlabel(u"Fréquence")
title("Réponse en fréquence (module)")
grid(b=True)
xlim([-Fe/2, Fe/2])
subplot(2,1,2)
plot(f,angle(H),label=u"Réponse en fréquence")
xlabel(u"Fréquence")
title("Réponse en fréquence (phase)")
grid(b=True)
xlim([-Fe/2, Fe/2])
fig.tight_layout() # évide le recouvrement des titles et labels
# Valeur en f=x : on cherche par find(f==x)
print ("Valeur en 0 : ",H[find(f==0)].real)
print ("Valeur en Fe/2 : ",H[find(f==-Fe/2)].real)
print ("A comparer avec les valeurs théoriques")
Créez une sinusoïde x, à la fréquence f0 = 3, échantillonnée à Fe = 32, sur 128 points
Filtrez cette sinusoïde par le filtre précédent
– en utilisant la fonction filter, y1=lfilter([1],[1 -0.8],x);
– en utilisant une convolution, y2=lfilter(h,1,x); avec \(h\) la réponse impulsionnelle du filtre avec \(a = 0.8\)
Expliquez pourquoi ce dernier calcul correspond effectivement à une convolution. Comparez ces deux résultats.
# Création d'une petite sinusoïde
N, fo, Fe = 128, 3, 32
t=arange(N)/Fe
x=sin(2*pi*fo*t)
figure(3)
plot(t,x)
xlabel("Temps")
grid(b=True)
ylim([-1.2, 1.2])
# Filtrée par le filtre h
a=0.8
h=ri([1],[1, -a],N) # recalculée sur N points
y1=lfilter([1],[1, -0.8],x)
y2=lfilter(h,[1],x)
figure()
plot(t,y1,label='y1')
plot(t,y2,label='y2')
grid(b=True)
legend()
show()
On peut aussi afficher la différence des deux courbes, comme ça c'est clair !
figure()
plot(t,y1-y2,label='y1-y2')
xlabel("Temps")
grid(b=True)
legend()
On va maintenant vérifier le théorème de Plancherel qui indique que la TF du produit de convolution est le produit des TF. On observera simplement que la sortie calculée comme la TF inverse du produit de la fonction de transfert H et de la TF X du signal d'entrée est identique (ou au moins très proche) de la sortie calculée par convolution/équations aux différences
y3=real(ifft(fft(h)*fft(x)))
plot(t,y3,label='y3')
plot(t,y2,label='y2')
legend()
La différence que l'on observe au début des deux signaux provient d'une différence d'hypothèse sur les valeurs du signal pour les temps négatifs. En réalité, la fonction lfilter suppose le signal nul hors des données spécifiées, ce qui entraîne une réponse transitoire. La TF, calculée par la fft, suppose implicitement que les signaux sont périodiques, et donc périodisés, hors de l'intervalle d'observation. On reveindra là-dessus plus tard en cours.
X=fftshift(fft(x))
H=fftshift(fft(h))
M=len(x)
f=arange(M)/M*Fe -Fe/2
plot(f,abs(H),color='green',label="H")
stem(f,abs(X)*6/M,markerfmt='b^',label="X")
xlim([-16, 16])
xlabel("Fréquence")
legend()
La sinusoïde est de fréquence \(fo=3\). Mesurons les valeurs du gain et du déphasage à cette fréquence :
H3=H[find(f==3)]
print("Valeur du gain complexe :", H3)
print("Module :", abs(H3))
print("Phase (en degrés):", angle(H3)/pi*180)
Puis, voyons ceci sur les courbes temporelles
figure()
plot(t,x,t,y3)
grid('on')
Mesure du déphasage : on mesure le retard entre les deux signaux
figure()
plot(t,x,label="x")
plot(t,y3,label="y3")
legend()
grid('on')
xlim([0, 0.4])
deltaT=min(find(y3>0))/Fe # x commence à zéro, le retard est donné par la première valeur où y3 devient >0
print("La valeur du déphasage, en degrés, est de ", (2*pi*fo)*deltaT/pi*180,"°")
Observations : On observe donc que si l'entrée est une sinusoïde, alors la sortie est également une sinusoïde, à un gain et déphasage près. Ce gain et ce déphasage correspondent exactement au gain et au déphasage apportés par la fonction de transfert en fréquence.
On renouvelle l'expérience, avec cette fois-ci un train d'impulsions plutôt qu'un sinusoïde. Le but est simplement de constater que cette fois-ci, la sortie du filtre est déformée : seuls les sinus (et cos) sont invariants par filtrage linéaire -- ce sont les fonctions propres des systèmes linéaires.
def rectpulse(x):
"""rectpulse(x): \n
Renvoie un ndarray contenant un train d'impulsions\n
rectangulaires, de période 2pi"""
return sign(sin(x))
x=rectpulse(2*pi*fo*t)
y=lfilter(h,[1],x)
figure()
plot(t,y)
grid(b=True)
Allez et pour le fun, on regarde ce que celà donne avec un rectangle plus large. On distingura en particulier temps de montée et de descente.
f1=0.5
x=rectpulse(2*pi*f1*t)
y=(1-a)*lfilter(h,[1],x) # Question subsidiaire: expliquez pourquoi on met ici un facteur (1-a) !
figure()
plot(t,y,label='y')
plot(t,x,label='x')
ylim([-1.2, 1.2])
grid(b=True)
title('Réponse du filtre à une entrée rectangulaire')