Esercizio - laboratorio 4, coefficienti di Fourier con la FFT
In questa pagina 4
Testo (laboratorio 4 del corso Teoria dei Segnali, UniPD, file lab4.m, parti 9-13). (9-10) Campionare un periodo ( s) del segnale con campioni e calcolarne i coefficienti di Fourier con la FFT, con la scalatura , l'asse delle frequenze e il riordino delle due metà del vettore. (11-13) Fare lo stesso per un'onda quadra (1 nella prima metà del periodo, 0 nella seconda) con e poi con campioni per periodo, confrontando con i coefficienti analitici (in modulo) e spiegando che cosa cambia con .
Teoria usata: Serie di FourierUn segnale periodico di periodo $T_p$ si scrive come somma di esponenziali alle frequenze multiple della fondamentale $F=1/T_p$: $s(t)=\sum_nS_ne^{i2\pi nFt}$, con $S_n=\frac1{T_p}\int_{T_p}s(t)e^{-i2\pi nFt}dt$. Si basa sull'ortogonalità degli esponenziali su un periodo. $S_0$ è il valor medio; per segnali reali $S_{-n}=S_n^*$ e si passa alla forma con coseni e seni; vale il teorema di Parseval $P=\sum|S_n|^2$. La convoluzione ciclica diventa il prodotto $T_pX_nY_n$ dei coefficienti. Le somme troncate presentano il fenomeno di Gibbs vicino ai salti.Serie di Fourier →, Trasformata di Fourier discreta (DFT) e FFTUn segnale discreto periodico di periodo $NT$ ha una trasformata discreta e periodica, la DFT: $S(kF)=\sum_{n=0}^{N-1}T,s(nT)e^{-i2\pi kn/N}$, con $F=1/(NT)$, e $s(nT)=\sum_{k=0}^{N-1}F,S(kF)e^{i2\pi kn/N}$. Dipende da soli $N$ numeri, si calcola senza approssimazioni e con la FFT costa $N\log_2N$ invece di $N^2$. I coefficienti di Fourier del segnale periodico sono $S_k=F,S(kF)$. La DFT dà anche campioni della trasformata di un segnale discreto o continuo a durata limitata (con zero-padding), ma la scalatura $T$ e l'asse delle frequenze vanno gestiti con cura.Trasformata di Fourier discreta (DFT) e FFT →, Campionamento e ricostruzioneIl campionamento $s_c(nT)=s(nT)$ trasforma un segnale continuo in uno discreto e, in frequenza, ripete lo spettro con periodo $F_c=1/T$: $S_c(f)=\sum_kS(f-kF_c)$. Se le repliche si sovrappongono si ha aliasing e il segnale non è recuperabile. Un interpolatore $\mathbb Z(T)\to\mathbb R$ con risposta impulsiva $g$ produce $\tilde s(t)=\sum_nT g(t-nT)s(nT)$ e $\tilde S=G,S_c$. Teorema del campionamento: se $s$ ha banda $B$ e $F_c\ge2B$, l'interpolatore ideale $G=\operatorname{rect}(f/F_c)$ ricostruisce esattamente $s(t)=\sum s(nT)\operatorname{sinc}(F_c(t-nT))$. Se le ipotesi non valgono c'è un errore, in banda e fuori banda, riducibile con un prefiltro anti-aliasing.Campionamento e ricostruzione → (aliasing), Segnali notevoli - gradino, rect, tri, sinc ed esponenzialiI segnali di uso più frequente sono la costante, la sinusoide $A_0\cos(2\pi f_0t+\varphi_0)$ e l'esponenziale complesso $Ae^{i2\pi f_0t}$ (periodici, a potenza finita), il gradino $\mathbf 1(t)$ e il segno, e gli impulsi a energia finita: $\operatorname{rect}$ (area $D$), $\operatorname{tri}$, $\operatorname{sinc}$ (area $1$), la gaussiana $e^{-\pi t^2}$ e gli esponenziali smorzati. Per ciascuno si sanno a memoria forma, area ed energia; gli altri segnali si ottengono da questi con traslazioni, scalature, somme e differenze.Segnali notevoli - gradino, rect, tri, sinc ed esponenziali → (il ).
Cosa si vuole calcolare e perché
Un segnale periodico di periodo ha lo sviluppo in serie , , con coefficienti (Serie di FourierUn segnale periodico di periodo $T_p$ si scrive come somma di esponenziali alle frequenze multiple della fondamentale $F=1/T_p$: $s(t)=\sum_nS_ne^{i2\pi nFt}$, con $S_n=\frac1{T_p}\int_{T_p}s(t)e^{-i2\pi nFt}dt$. Si basa sull'ortogonalità degli esponenziali su un periodo. $S_0$ è il valor medio; per segnali reali $S_{-n}=S_n^*$ e si passa alla forma con coseni e seni; vale il teorema di Parseval $P=\sum|S_n|^2$. La convoluzione ciclica diventa il prodotto $T_pX_nY_n$ dei coefficienti. Le somme troncate presentano il fenomeno di Gibbs vicino ai salti.Serie di Fourier →)
Campionando un periodo con campioni equispaziati , , l'integrale diventa una somma, , cioè
(fft(s)[n] è proprio ). Il fattore è lo stesso del laboratorio 3: la FFT dà direttamente i coefficienti di Fourier , ognuno associato alla frequenza . Resta da capire l'asse delle frequenze (a quali corrispondono gli elementi del vettore) e quanto è fedele il risultato al coefficiente vero: la risposta sta nell'aliasing.
Parti 9-10 - un coseno
import numpy as np
import matplotlib.pyplot as plt
# PARTE 9 - un periodo di coseno campionato: N = 30 campioni, periodo Tp = 1 s
N = 30
Tp = 1
dt = Tp / N # passo di campionamento: 1/30 s
t = np.arange(N) * dt # istanti 0, dt, ..., (N-1) dt (l'istante Tp e' ESCLUSO: apparterrebbe al periodo dopo)
fcos = 1 # frequenza del coseno: 1 Hz = 1/Tp
s = np.cos(2 * np.pi * fcos * t)
plt.figure()
plt.plot(t, s)
plt.xlabel('t [s]'); plt.grid(True)
plt.show()
print(f"dt = {dt:.4f} s, campioni per periodo = {N}, frequenza di campionamento Fp = {1 / dt:.0f} Hz")Risultato:
dt = 0.0333 s, campioni per periodo = 30, frequenza di campionamento Fp = 30 HzIl coseno ha frequenza Hz : un periodo esatto nei campioni. Il vettore t va da a s: l'istante è già il primo del periodo successivo e si lascia fuori (altrimenti il periodo avrebbe campioni). Ci sono campioni per periodo, cioè una frequenza di campionamento Hz.
# PARTE 10 - coefficienti di Fourier con la fft
S = dt / Tp * np.fft.fft(s) # dt/Tp = 1/N: coefficienti S_n = (1/N) somma di s e^(-i 2 pi n m / N)
F = 1 / Tp # passo in frequenza = 1/Tp = 1 Hz
f = np.arange(-N // 2, N // 2) * F # -15, -14, ..., 14 Hz
S = np.concatenate((S[N // 2:], S[:N // 2])) # riordino: [S(N/2+1:N), S(1:N/2)] in MATLAB; equivale a np.fft.fftshift(S)
plt.figure()
plt.stem(f, np.abs(S), basefmt=' ')
plt.xlabel('Frequenza [Hz]'); plt.ylabel('Modulo coeff. Fourier'); plt.ylim(-0.5, 1); plt.grid(True)
plt.show()
print("asse:", f[0], "...", f[-1], "Hz (", len(f), "righe )")
print("righe con |S| > 1e-9:", [(float(ff), round(float(abs(c)), 6)) for ff, c in zip(f, S) if abs(c) > 1e-9])
print("valori complessi in -1 e +1 Hz:", np.round(S[f == -1][0], 6), np.round(S[f == 1][0], 6))
print("uguale a fftshift:", np.allclose(S, np.fft.fftshift(dt / Tp * np.fft.fft(s))))Risultato:
asse: -15.0 ... 14.0 Hz ( 30 righe )
righe con |S| > 1e-9: [(-1.0, 0.5), (1.0, 0.5)]
valori complessi in -1 e +1 Hz: (0.5+0j) (0.5-0j)
uguale a fftshift: TrueI coefficienti. Si ha (formula di Eulero), quindi e tutti gli altri coefficienti sono nulli. È proprio ciò che stampa il codice: due sole righe, a Hz, ciascuna di valore (e reale: la fase è nulla perché il coseno parte da un massimo). Una sinusoide reale ha sempre due righe, simmetriche (il segnale è reale: ).
L'asse delle frequenze e il riordino. La fft restituisce numeri, indicati dall'indice . Per la periodicità della DFT, l'indice e l'indice rappresentano la stessa riga: le righe da a sono in realtà quelle a frequenza negativa . Per avere l'asse da a Hz si mettono prima le posizioni da a e dopo quelle da a : np.concatenate((S[N//2:], S[:N//2])), in MATLAB [S(N/2+1:N), S(1:N/2)]. È lo scambio delle due metà, cioè np.fft.fftshift (ultima riga stampata). L'asse np.arange(-N//2, N//2) * F ha punti, da a (con pari c'è una riga in più a sinistra, Hz , che è il "doppione" di Hz). Il massimo ordinabile è Hz: oltre non si vede niente, è il teorema del campionamento (Campionamento e ricostruzioneIl campionamento $s_c(nT)=s(nT)$ trasforma un segnale continuo in uno discreto e, in frequenza, ripete lo spettro con periodo $F_c=1/T$: $S_c(f)=\sum_kS(f-kF_c)$. Se le repliche si sovrappongono si ha aliasing e il segnale non è recuperabile. Un interpolatore $\mathbb Z(T)\to\mathbb R$ con risposta impulsiva $g$ produce $\tilde s(t)=\sum_nT g(t-nT)s(nT)$ e $\tilde S=G,S_c$. Teorema del campionamento: se $s$ ha banda $B$ e $F_c\ge2B$, l'interpolatore ideale $G=\operatorname{rect}(f/F_c)$ ricostruisce esattamente $s(t)=\sum s(nT)\operatorname{sinc}(F_c(t-nT))$. Se le ipotesi non valgono c'è un errore, in banda e fuori banda, riducibile con un prefiltro anti-aliasing.Campionamento e ricostruzione →).
Grafico interattivo: Coefficienti di Fourier del coseno cos(2πt) (periodo 1 s): due righe di altezza 1/2 in n = ±1, tutte le altre nulle; è quello che calcola la FFT con N = 30 campioni (a meno di 10^-16)
Parti 11-13 - onda quadra, 30 campioni e 150 campioni
# PARTE 11-12 - onda quadra: 1 nella prima meta' del periodo, 0 nella seconda; N = 30
def onda_quadra(N, Tp=1):
dt = Tp / N
t = np.arange(N) * dt
s = np.zeros(N)
s[:N // 2] = 1 # MATLAB s(1:N/2) = 1
s[N // 2:] = 0 # MATLAB s(N/2+1:N) = 0
S = dt / Tp * np.fft.fft(s)
S = np.concatenate((S[N // 2:], S[:N // 2])) # riordino delle due meta'
F = 1 / Tp
f = np.arange(-N // 2, N // 2) * F
return t, s, f, S
t30, s30, f30, S30 = onda_quadra(30)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(t30, s30); plt.ylim(-0.5, 1.5); plt.xlabel('t [s]'); plt.grid(True)
plt.subplot(1, 2, 2)
plt.stem(f30, np.abs(S30), basefmt=' '); plt.xlabel('Frequenza [Hz]'); plt.ylabel('Modulo coeff. Fourier'); plt.grid(True)
plt.show()
# analitico: S_n = (1/2) sinc(n/2) e^(-i pi n/2); in modulo (1/2) |sinc(n/2)|
def S_analitico(n):
return 0.5 * np.sinc(n / 2) * np.exp(-1j * np.pi * n / 2)
print(" n |S_n| con fft (1/2)|sinc(n/2)| differenza")
for n in [0, 1, 2, 3, 4, 5, 7, 9, 11, 14, -15]:
num = abs(S30[f30 == n][0])
an = abs(S_analitico(n))
print(f"{n:>4} {num:.5f} {an:.5f} {num - an:+.5f}")Risultato per :
n |S_n| con fft (1/2)|sinc(n/2)| differenza
0 0.50000 0.50000 +0.00000
1 0.31889 0.31831 +0.00058
2 0.00000 0.00000 -0.00000
3 0.10787 0.10610 +0.00177
4 0.00000 0.00000 -0.00000
5 0.06667 0.06366 +0.00300
7 0.04982 0.04547 +0.00434
9 0.04120 0.03537 +0.00583
11 0.03649 0.02894 +0.00755
14 0.00000 0.00000 -0.00000
-15 0.03333 0.02122 +0.01211L'onda quadra vale per e per (non è centrata nell'origine): i primi 15 campioni a 1, gli altri 15 a 0.
Grafico interattivo: L'onda quadra del laboratorio: 1 nella prima metà del periodo (0 ≤ t < 1/2) e 0 nella seconda, periodo 1 s, valor medio 1/2
Il valore analitico. I coefficienti sono
Il modulo è : vale per pari non nullo (lì ), per dispari: per , per , per , e così via come . La fase dipende solo da dove cade l'onda nel periodo (qui parte da ) e per questo si confrontano i moduli. Il valore medio è la media dei campioni, .
Grafico interattivo: Modulo dei coefficienti di Fourier dell'onda quadra: |S_n| = (1/2)|sinc(n/2)|, che vale 1/2 in n = 0, 1/(π|n|) per n dispari e zero per n pari non nullo (decade come 1/n: non si annulla mai, la banda è illimitata)
Cosa dà la FFT con . La tabella mostra che i valori sono vicini a quelli veri ma sempre un po' più grandi: contro per (errore ), contro per (16%), e nell'ultima riga, , contro (57%). Le righe pari sono zero (esattamente, come nel caso vero).
Perché c'è un errore: l'aliasing. L'onda quadra ha infinite armoniche, con che non si annullano mai: il suo spettro non è a banda limitata. Campionare con punti per periodo equivale a campionare nel tempo con frequenza , e questo ripete lo spettro ogni righe e somma le code ripetute (formula del campionamento , Campionamento e ricostruzioneIl campionamento $s_c(nT)=s(nT)$ trasforma un segnale continuo in uno discreto e, in frequenza, ripete lo spettro con periodo $F_c=1/T$: $S_c(f)=\sum_kS(f-kF_c)$. Se le repliche si sovrappongono si ha aliasing e il segnale non è recuperabile. Un interpolatore $\mathbb Z(T)\to\mathbb R$ con risposta impulsiva $g$ produce $\tilde s(t)=\sum_nT g(t-nT)s(nT)$ e $\tilde S=G,S_c$. Teorema del campionamento: se $s$ ha banda $B$ e $F_c\ge2B$, l'interpolatore ideale $G=\operatorname{rect}(f/F_c)$ ricostruisce esattamente $s(t)=\sum s(nT)\operatorname{sinc}(F_c(t-nT))$. Se le ipotesi non valgono c'è un errore, in banda e fuori banda, riducibile con un prefiltro anti-aliasing.Campionamento e ricostruzione →):
Alla riga si aggiungono i contributi delle righe vere , tutte piccole ma non nulle. Più è vicino al bordo , più sono grandi: per la riga vera ha indice quasi uguale in modulo a , quindi ampiezza paragonabile. Il blocco finale della sezione lo verifica numericamente: se nei due punti di salto ( e ) si mettono i valori di mezzo (, regola dell'emivalore) la fft coincide con la somma delle code fino a . Il laboratorio mette invece in e in : questo aggiunge ai coefficienti (cioè ai dispari), un altro errore dell'ordine di .
# PARTE 12 (correzione) e 13 - stessa onda quadra con N = 150 campioni per periodo
t150, s150, f150, S150 = onda_quadra(150)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(t150, s150); plt.ylim(-0.5, 1.5); plt.xlabel('t [s]'); plt.grid(True)
plt.subplot(1, 2, 2)
plt.stem(f150, np.abs(S150), basefmt=' '); plt.xlabel('Frequenza [Hz]'); plt.ylabel('Modulo coeff. Fourier'); plt.grid(True)
plt.show()
print("asse con N = 150:", f150[0], "...", f150[-1], "Hz (", len(f150), "righe )")
print(" n N=30: fft errore N=150: fft errore (1/2)|sinc(n/2)|")
for n in [0, 1, 3, 5, 7, 9, 11]:
a30 = abs(S30[f30 == n][0]); a150 = abs(S150[f150 == n][0]); an = abs(S_analitico(n))
print(f"{n:>4} {a30:.5f} {a30 - an:+.5f} {a150:.5f} {a150 - an:+.5f} {an:.5f}")
print("righe pari (n = 2, 4, 6) con N=30 e N=150:", np.abs(S30[[f30 == 2][0]]).round(12), np.abs(S150[[f150 == 2][0]]).round(12))
print(f"ultima riga (n = -N/2): N=30: {abs(S30[0]):.5f} contro {abs(S_analitico(-15)):.5f}; N=150: {abs(S150[0]):.5f} contro {abs(S_analitico(-75)):.5f}")Risultato per :
asse con N = 150: -75.0 ... 74.0 Hz ( 150 righe )
n N=30: fft errore N=150: fft errore (1/2)|sinc(n/2)|
0 0.50000 +0.00000 0.50000 +0.00000 0.50000
1 0.31889 +0.00058 0.31833 +0.00002 0.31831
3 0.10787 +0.00177 0.10617 +0.00007 0.10610
5 0.06667 +0.00300 0.06378 +0.00012 0.06366
7 0.04982 +0.00434 0.04564 +0.00016 0.04547
9 0.04120 +0.00583 0.03558 +0.00021 0.03537
11 0.03649 +0.00755 0.02919 +0.00026 0.02894
righe pari (n = 2, 4, 6) con N=30 e N=150: [0.] [0.]
ultima riga (n = -N/2): N=30: 0.03333 contro 0.02122; N=150: 0.00667 contro 0.00424Cosa cambia con . Ci sono tre effetti dell'avere più campioni per periodo, a parità di s (stessa Hz):
- L'asse delle frequenze si allarga. I punti sono , sempre a distanza Hz, quindi l'asse va da a Hz, cioè fino a : la frequenza di campionamento è cresciuta da a Hz e con essa il limite di Nyquist. Si vedono armoniche invece di .
- L'aliasing diminuisce. Le armoniche vere che si ripiegano sulla riga sono quelle a distanza : più è grande, più sono lontane e più sono piccole (). L'errore massimo sulle prime armoniche () scende da () a (): contro per (errore ), contro per (errore ).
- Il difetto sul bordo resta, ma è più in alto e più piccolo. L'ultima riga () vale contro vero (sempre un errore del relativo, perché il confronto con le code è lo stesso) ma in valore assoluto è cinque volte più piccola: il modo giusto per fidarsi dei risultati è guardare le armoniche ben dentro all'asse, .
# Errore massimo sulle prime armoniche, e perche' l'errore c'e'
for N, f_, S_ in [(30, f30, S30), (150, f150, S150)]:
sel = np.abs(f_) <= 9
err = np.max(np.abs(np.abs(S_[sel]) - np.abs(S_analitico(f_[sel]))))
print(f"N = {N:>3}: errore massimo sulle righe con |n| <= 9: {err:.5f}")
# la fft di una forma d'onda campionata "ripete" lo spettro vero ogni N righe e somma le code (aliasing).
# Se nei due salti si mette il valore di meta' (0.5), il risultato e' esattamente la somma delle code:
N = 30
s_mezzo = np.zeros(N); s_mezzo[:N // 2] = 1; s_mezzo[0] = 0.5; s_mezzo[N // 2] = 0.5
S_mezzo = np.fft.fft(s_mezzo) / N
K = np.arange(-20000, 20001)
for n in [1, 3, 5]:
somma_code = np.sum(S_analitico(n + K * N)) # somma su k dei coefficienti veri S_(n + kN)
print(f"n = {n}: |fft| con valori di meta' = {abs(S_mezzo[n]):.6f} |somma delle code| = {abs(somma_code):.6f}")
# il laboratorio mette 1 in t = 0 e 0 in t = Tp/2: aggiunge 0.5/N in t=0 e toglie 0.5/N in t=Tp/2
S_lab = np.fft.fft(s30) / N
corr = (0.5 / N) * (1 - np.exp(-1j * np.pi * np.arange(N)))
print("S_lab = S_mezzo + (0.5/N)(1 - (-1)^n):", np.allclose(S_lab, S_mezzo + corr))N = 30: errore massimo sulle righe con |n| <= 9: 0.00583
N = 150: errore massimo sulle righe con |n| <= 9: 0.00021
n = 1: |fft| con valori di meta' = 0.317145 |somma delle code| = 0.317146
n = 3: |fft| con valori di meta' = 0.102589 |somma delle code| = 0.102590
n = 5: |fft| con valori di meta' = 0.057735 |somma delle code| = 0.057735
S_lab = S_mezzo + (0.5/N)(1 - (-1)^n): TrueLe parti 12 (correzione) e 13 del file MATLAB contengono lo stesso codice (il blocco è duplicato). Nel file, nel grafico del tempo, ylabel dice 'Modulo coeff. Fourier' e nel grafico in frequenza xlabel dice 'Frequenza' senza unità: sono etichette scambiate (nella parte 12 correzione e nella 13), non influiscono sul calcolo.
Controllo
- Coseno: righe in , altre .
- Onda quadra: ; pari nulli; dispari con errore da aliasing che passa da () a () per .
- La fft dei campioni con valori di mezzo ai salti coincide (a ) con .
Versione ripasso
- Scalatura. , , riga a frequenza .
- Asse. Gli indici sono frequenze negative (): si scambiano le due metà (
fftshift) e l'asse è (, ). - Coseno (): , altro . Onda quadra (1 su metà periodo): , dispari, pari.
- Aliasing. Banda illimitata: la fft dà , un po' maggiore del vero e peggiore vicino a ; con l'asse va a Hz e l'errore sulle prime armoniche cala da a .