Il principio su cui si basa l'analisi di Fourier è la constatazione del fatto che spesso in natura un segnale oscillante si presenta non come una oscillazione ad una singola frequenza (o lunghezza d'onda) "pura", ma come una sovrapposizione di frequenze (o lunghezze d'onda) differenti. Esempi classici si trovano nell'ottica (la luce bianca è una sovrapposizione di differenti colori, cioè frequenze, che possono essere decomposte facendo passare la luce attraverso un prisma), in musica (i suoni "armonici" non sono altro che la sovrapposizione di una frequenza fondamentale del suono, con sovrapposte tutte le frequenze multiple della fondamentale), nelle comunicazioni (un segnale radio, o TV, è costituito da un segnale "portante" ad alta frequenza, che serve a trasportare il segnale a grandi distanze, con sovrapposta una modulazione a bassa frequenza, che è il segnale vero e proprio, da filtrare dal resto per poterlo estrarre), ecc.
Lo scopo dell'analisi di Fourier è di dare un metodo per separare le varie frequenze contenute in un segnale e analizzare qual'è il contributo delle singole frequenze alla ricostruzione del segnale di partenza.
Data una funzione \(f(t)\) a quadrato sommabile, periodica su un periodo \(T\), si può sempre scrivere che: \[
f(t) = a_0 + \sum_{n=1}^{+\infty} a_n \cos\left( \frac{2\pi}{T} nt \right) + b_n \sin\left( \frac{2\pi}{T} nt \right)
\] Notiamo che le funzioni \(\cos\left( \frac{2\pi}{T} nt \right)\) e \(\sin\left( \frac{2\pi}{T} nt \right)\) sono funzioni che hanno presentano, nell'intervallo \(t \in [0,T]\), \(n\) oscillazioni!
Esempio (per \(T=2\pi\)):
import numpy as np
import matplotlib.pyplot as plt
x=np.linspace(0.0,2.0*np.pi,129)
%matplotlib inline
plt.subplot(221)
n=1
cx1=np.cos(n*x)
sx1=np.sin(n*x)
plt.title("n=1")
plt.plot(x,cx1)
plt.plot(x,sx1)
plt.subplot(222)
n=2
cx2=np.cos(n*x)
sx2=np.sin(n*x)
plt.title("n=2")
plt.plot(x,cx2)
plt.plot(x,sx2)
plt.subplot(223)
n=3
cx3=np.cos(n*x)
sx3=np.sin(n*x)
plt.title("n=3")
plt.plot(x,cx3)
plt.plot(x,sx3)
plt.subplot(224)
n=4
cx4=np.cos(n*x)
sx4=np.sin(n*x)
plt.title("n=4")
plt.plot(x,cx4)
plt.plot(x,sx4)
plt.show()
Quando ci si trova ad analizzare dati sperimentali, tuttavia, non si dispone dei valori del segnale \(\forall t\), ma in genere il segnale viene campionato su un insieme di punti discreto: \(t_j = j\Delta t\), con \(j=0,\ldots, N\) e \(\Delta t = T / N\). Il Teorema del campionamento (o di Nyquist-Shannon) afferma che, dato lo sviluppo di Fourier di una certa \(f(t)\) come:
\[ f(t) = \sum_{k=-\infty}^{+\infty} c_k e^{ik\omega t} \]
non tutti i valori di \(k\) (sia positivi che negativi) sono indipendenti tra di loro in un segnale campionato, ma solo quelli per cui \(-N_{max} \leq k \leq N_{max}\), dove \(N_{max} = N/2\), che viene detto Numero di Nyquist e la corrispondente frequenza: \(\omega_{max} = N_{max} \omega\), detta Frequenza di Nyquist del segnale campionato.
Qual'è il significato del teorema? Vuol dire che se io considero un segnale che contenga nel suo sviluppo di Fourier un valore di \(k\) pari a: \(k = N_{max} + m\), il contributo di quel \(k\) al segnale è identico a quello che darebbe un valore di \(k\) pari ad \(m-N_{max}\). Infatti, sui punti discreti \(t_j\):
\[ e^{ik\omega t_j} = e^{i(N_{max}+m)\omega t_j} = e^{iN_{max}\omega t_j} e^{im\omega t_j}\]
e ricordando che: \(\omega = \frac{2\pi}{T}\) e che: \(t_j = j \Delta t = \frac{jT}{N}\), si ha:
\[ e^{ik\omega t_j} = e^{i\frac{N}{2}\omega t_j} e^{im\omega t_j} = e^{i(N-\frac{N}{2})\frac{2\pi}{T}\frac{jT}{N}} e^{im\omega t_j} = e^{i2\pi j} e^{-i\frac{N}{2}\omega t_j} e^{im\omega t_j} = e^{i(m-N_{max})\omega t_j}\]
quindi, l'onda con frequenza \(k\omega\) passa per gli stessi punti di un'onda con frequenza (negativa!) \((m-N_{max})\omega\).
Esempio: nell'esempio seguente, si scelgono: \(\omega=1\) (\(T=2\pi\)), \(N=32\) punti di campionamento e quindi \(N_{max}=16\). Si noti come per \(m=12\) la funzione \(\cos(k\omega t)\) (linea blu), con \(k=m+N_{max}\) e la funzione \(\cos(k_a\omega t)\) (linea verde), con \(k_a=m-N_{max}\), quando vengono rappresentate su \(N=32\) punti discreti, si intersecano negli stessi punti (rappresentati dai pallini gialli per la prima funzione e dai triangoli blu per la seconda)...
T = 2.0 * np.pi
N = 32
NN = 1000
Nmax = N / 2
m = 12
k = m + Nmax
ka = m - Nmax
x = np.linspace( 0.0, T, N + 1 )
xx = np.linspace( 0.0, T, NN + 1 )
yk = np.cos(k*x)
ykk = np.cos(k*xx)
yka = np.zeros(N+1)
ykka = np.zeros(NN+1)
for j in range(0,N+1):
yka[j] = np.cos(ka*x[j])
for j in range(0,NN+1):
ykka[j] = np.cos(ka*xx[j])
plt.plot(x,yk,'yo',markersize=20)
plt.plot(xx,ykk,'-')
plt.plot(x,yka,'b^',markersize=10)
plt.plot(xx,ykka,'-')
plt.show()
Notiamo come il calcolo dei coefficienti \(c_k\) debba essere effettuato solo per i valori \(k>0\), essendo valida la proprietà : \(c_{-k} = c^*_k\). Questo equivale a calcolare \(c_k\) per \(k=0,\ldots,N/2\), cioé \(N/2+1\) valori complessi. In realtà , poiché \(c_0\) è reale risulta inutile calcolarne la parte immaginaria nulla. Si può inoltre far vedere che anche \(c_{N/2}\) è reale, poiché:
\[ c_{N/2} = \frac{h}{T} \sum_{j=0}^{N-1} F_k(t_j) = \frac{h}{T} \sum_{j=0}^{N-1} f(t_j) e^{i\frac{N}{2}\frac{2\pi}{T}\frac{jT}{N}} = \frac{h}{T} \sum_{j=0}^{N-1} f(t_j) e^{i\pi j} = \frac{h}{T} \sum_{j=0}^{N-1} (-1)^j f(t_j) \]
Di conseguenza, i valori effettivi da calcolare sono: \(2(N/2+1) - 2 = N\), in quanto le parti immaginarie (nulle!) di \(c_0\) e \(c_{N/2}\) non necessitano di essere calcolate. Siccome il calcolo richiede quindi la valutazione per \(N/2\) volte della funzione complessa: \(F_k(t_j),\ k=0,\ldots,N/2,\ j=0,\ldots,N-1\), in totale dovremo effettuare \(N^2\) prodotti per valutare i coefficienti indipendenti. Esistono algoritmi chiamati FFT (Fast Fourier Transform), che permettono di effettuare tale valutazione effettuando circa \(N\log_2 N\) prodotti, che per \(N\) grandi fa una enorme differenza!
rfft(A), irfft(A), rfft2(A), irfft2(A), rfftn(A), irfftn(A)che calcolano la trasformata di un vettore reale A 1, 2, ..., n - dimensionale, e le analoghe funzioni per vettori complessi:
fft(A), ifft(A), fft2(A), ifft2(A), fftn(A), ifftn(A)Esistono poi librerie specifiche (es. pyFFTW) che forniscono funzioni analoghe ma con performances di calcolo migliori.
Esempio: trasformata della funzione \(f(t) = \sin(x) + \cos(5x)\) su N=64 punti discreti...
N = 64 # Numero di intervalli di discretizzazione
dpi = 2.0 * np.pi # 2 pi greco
dx = dpi / N # Spaziatura degli intervalli discretizzati
x = np.arange( 0.0, dpi, dx ) # Genera un vettore di 128 punti con valori tra 0 e 2 pi - dx
y = np.sin(x) + np.cos(5.0*x) # Funzione di cui calcolare la trasformata
plt.xlabel(r"$t$")
plt.ylabel(r"$f(t)$")
plt.title(r"$f(t)=\sin(x)+\cos(5x)$")
plt.plot(x,y,'-')
plt.show()
ak = np.fft.rfft(y)
print(ak)
print(len(ak))
Scriviamo ora uno script che calcoli lo spettro di una data funzione nota su N punti:
def spettro(f):
N = len(f) # Lunghezza del vettore di partenza
Nf = int( N / 2 ) # Frequenza di Nyquist
om = np.linspace(0.0, Nf, Nf+1) # Vettore di output contenente le frequenze
sp = np.zeros(Nf+1) # Vettore di output contenente lo spettro
ak = np.fft.rfft(f) / N # Trasformata del vettore di partenza (normalizzata!)
for n in range(Nf+1):
if n == 0:
sp[n] = np.square(np.abs(ak[n]))
else:
sp[n] = 2.0 * np.square(np.abs(ak[n]))
return om, sp
w, Ew = spettro(y)
plt.title("Spettro")
plt.xlabel(r"$\omega$")
plt.ylabel(r"$E(w)$")
plt.plot(w,Ew,'-')
plt.show()
Esempio:
generiamo una funzione che abbia uno spettro con tutte le armoniche e ricostruiamo il segnale di partenza usando solo le prime \(n \le N/2\) armoniche del segnale, per vedere come le varie armoniche contribuiscono al segnale stesso.
Scegliamo come funzione di cui calcolare lo spettro l'impulso triangolare:
\[ f(x) =
\begin{cases}
x & \textrm{per} \qquad 0 \le x < \pi; \\
0 & \textrm{per} \qquad x = \pi; \\
x-2\pi & \textrm{per} \qquad \pi < x < 2\pi \\
\end{cases}
\]
N2 = int( N / 2 )
y[0:N2] = x[0:N2]
y[N2] = 0.0
y[N2+1:] = x[N2+1:] - dpi
plt.title("Impulso triangolare")
plt.xlabel(r"$t$")
plt.ylabel(r"$f(t)$")
plt.plot(x,y,'-')
plt.show()
w, Ew = spettro(y)
plt.title("Spettro")
plt.xlabel(r"$\omega$")
plt.ylabel(r"$E(\omega)$")
plt.xscale("log") # Spettro in scala log-log
plt.yscale("log")
wteo = w[1:N2]**(-2.0)
plt.ylim(1.e-5,10)
plt.plot( w, Ew, '-', w[1:N2], wteo, 'g-' )
plt.show()
Definiamo quindi una funzione che effettua la ricostruzione del segnale (trasformata inversa) con un numero di armoniche \(n \le N/2\):
def reconstruct( ak, n ):
Nyq = len( ak ) - 1 # Frequenza di Nyquist
Ns = 2 * Nyq # Numero di punti nel vettore di output
sr = np.zeros( Ns ) # Vettore di output
if ( n > Nyq ):
print("Errore! Il numero di armoniche da utilizzare per la ricostruzione deve essere minore della frequenza di")
print("Nyquist del segnale!")
return sr
else:
akk = np.zeros(Nyq+1, dtype=complex)
akk[:n+1] = ak[:n+1]
sr = np.fft.irfft(akk)
return sr
ak = np.fft.rfft(y) # Calcolo dei coefficienti di Fourier
sr3 = reconstruct(ak,3)
plt.title("Segnale di partenza e ricostruito con 3 armoniche")
plt.xlabel(r"$t$")
plt.ylabel(r"$f(t), f_3(t)$")
plt.plot(x,y,'b-',x,sr3,'g-')
plt.show()
sr8 = reconstruct(ak,8)
plt.title("Segnale di partenza e ricostruito con 8 armoniche")
plt.xlabel(r"$t$")
plt.ylabel(r"$f(t), f_8(t)$")
plt.plot(x,y,'b-',x,sr8,'g-')
plt.show()
sr32 = reconstruct(ak,32)
plt.title("Segnale di partenza e ricostruito con 32 armoniche")
plt.xlabel(r"$t$")
plt.ylabel(r"$f(t), f_{32}(t)$")
plt.plot(x,y,'b-',x,sr32,'g-')
plt.show()