3.3 DFT - Propiedades - FFT con Python

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]
..


1. Periódicidad

Referencia: McClellan 8.2.1 p311

La DFT de una secuencia finita de N-puntos es una versión discreta de DTFT:

X[k] = \sum_{n=0}^{N-1} x[n] e^{-j(2\pi k/N)} = X\Big( e^{j\omega} \Big) \Big|_{\omega=(2\pi k/N)} = X\Big( e^{j (2\pi k/N)}\Big)

k= 0, 1, ..., N-1

Dado que DTFT X(e) es siempre periódica con periodo 2π, la DFT X[k] debe ser por lo tanto también periódica. Esto implica que el índice k se mantiene siempre entre 0 y N-1, sin afectar ser evaluado para k≥N o k<0.

Considere que ω + 2π = 2πk/N + 2π(N/N) = 2π(k+N)/N = ωk+N
que implica que los coeficientes X[k] tienen periodo igual a N. Por lo que no se considera necesario más cálculos X[k] fuera del intervalo 0≤k<N.

X[k] = X[k+N]

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]
..


2. Frecuencias Negativas y DFT

Referencia: McClellan 8.2.2 p311, 8.2.3.1 p313

Todos los resultados de DFT y IDFT tienen índices no negativos por ser convenientes para los cálculos y expresiones matemáticas. Para la gráfica de espectro de frecuencias se espera utilizar frecuencias positivas y negativas, con simetría en las conjugadas cuando la señal es tipo real.

Cuando la DFT de tamaño N, siendo N par, la transformada X[k] tiene un valor a k=N/2. Cuando N es impar, lo indicado no aplica. El índice k=N/Se  corresponde a una frecuencia normalizada ω=2π(N/2)/N)=π. Sin embargo los valores en el espectro son de periodos 2π, siendo ω=-π un alias de ω=π.

Si se realiza la gráfica de forma centrada, la frecuencia esta entre [-π, π]

En Python el desplazamiento de los índices se puede realizar usando np.fft.fftshift() que ubica el centro en la muestra N/2, siempre que N sea par.

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]
..


3. Simetría conjugada de DFT

Para una señal x[n] de tipo real, existe la simetría conjugada en la DTFT, los coeficientes deben satisfacer la propiedad:

X[-1]  = X*[1]
X[-2]  = X*[2]
...
X[-k]  = X*[k]

La DFT de una señal real satisface:

X[N-k] = X*[k]
k = 0, 1, ..., N-1

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]
..


4. Relleno de ceros - Zero Padding

Referencia: McClellan 8.2.4 p314

Para suavizar una gráfica de una DTFT como respuesta de frecuencia, se necesita muestras de frecuencia con tamaños de paso pequeños. De forma implícita, la DFT asume que la longitud o tamaño de la transformada es igual al tamaño  o longitud de la señal L, por lo que la DFT de L puntos calcula las muestras de DTFT para (ω=2π/L)k, siendo k=0,1,...,L-1. Las L muestras de frecuencia son con espacios iguales en el intervalo 0≤ω≤2π, con resultado:

H[k] = H \Big( e^{j(2\pi /L)k} \Big) = H \Big( e^{j\omega}\Big) \Big|_{\omega=(2\pi/L)/k}

k = 0, 1, ..., L-1

Para disponer de  muestras de frecuencia a espacios menores que (2π/N) donde N>L, una forma sencilla es rellenar con ceros o zero-padding. Con lo que la señal se alarga antes de realizar el cálculo para la DFT de N puntos.

h_{zp}[n] = \begin{cases} h[n] & n=\text {0, 1, ..., L-1} \\0 & n= L, L+1, ... , N-1 \end{cases}

4.1 Ejemplo: Respuesta de frecuencias con DFT con zero-padding

Referencia: McClellan 8.4 p315

Suponer que requiere una gráfica de respuesta de frecuencia de un filtro FIR de 8 puntos. Para evaluar la respuesta de frecuencia con mejor resolución mediante tamaños de paso mas pequeños entre frecuencias, se usará relleno con ceros o zero-padding, usando un número par en N, por ejemplo N=80.

fft rect zeropadding L=16 N=80
En el caso que N=40, el resultado tiene menor resolución considerando que el intervalo ω se mantiene entre [-π, π]

fft rect zero padding L=16 N=40

4.2 Algoritmo en Python

El filtro FIR se puede realizar usando un rectángulo con funciones escalón o Heaviside(n), luego evaluando para las muestras ti.

Para obtener la DFT se usa np.fft.fft(xn, n=N), si n es menor que el tamaño de la señal de entrada, la entrada se trunca. Si  es mas grande, se usa zero-padding para completar las muestras. Por lo que la instrucción de Numpy contempla el uso de zero-padding, lo que simplifica el uso del algoritmo presentado de forma didáctica paso a paso.

El centrado del espectro de frecuencias se puede realizar usando un desplazamiento de índices ki_shift.

El resultado del algoritmo, con X[k] centrada, se presenta como:

k0: 0 ; i: 40
freq_Hz: 0.0 ; Xk[k0]: (8+0j)
[ i, freq[ki],|X[k]| ,X[k]]
[0, 0.0, 8.0, (8+0j)]
[1, 0.0125, 7.871076020105053, (7.575558332074842-2.1365284158195137j)]
[2, 0.025, 7.491613901992368, (6.387650908672175-3.914357511197061j)]
[3, 0.037500000000000006, 6.883060301425795, (4.672226464156024-5.054386113140136j)]
...

Instrucciones en Python

# ejemplo 8.4 p315 DFT zero-padding
# telg1034 DSP fiec-espol edelros@espol.edu.ec
import numpy as np
import matplotlib.pyplot as plt

# INGRESO
L = 16   # Tramos en un periodo

# señal es rectangular periódico rect[n]
f0 = 1/16  # frecuencia fundamental de señal
ancho = 0.5*(1/f0) # ancho del impulso rectangular
xt = lambda t: np.heaviside(t,1)-np.heaviside(t-ancho,1)

# para DFT
N = 80 #2*L # N par mayor que L, tramos DFT

# PROCEDIMIENTO
# muestreo de señal x[n]
t0 = 0 ; tn = 1*(1/f0) # intervalo [t0,tn]
dt = (tn-t0)/L
ti = np.arange(t0,tn,dt)
xn = xt(ti)

# verificar que L<=N
if L<=N:
    # DFT transformada rapida de Fourier
    Xk = np.fft.fft(xn,n=N)
    Xkmagn = np.abs(Xk) # para gráfica
    Xkfase = np.angle(Xk)
    freq_Hz = np.fft.fftfreq(N,dt) # frecuencias en Hz

    mitad = int(N/2)
    k0 = np.argmax(Xk) # Pico |X[k]|

    # ki indices de orden
    ki = np.arange(0,N,1)
    ki_shift = np.copy(ki)
    ki_shift[mitad:] = ki[mitad:]-N

# SALIDA
if N>=L:
    print('k0:',k0,'; i:',mitad+k0)
    print('freq_Hz:',freq_Hz[k0],
          '; Xk[k0]:',Xk[k0])
    print('[ i, freq[ki],|X[k]| ,X[k]]')
    kmin = k0-4 ; kmax = k0+4
    if kmin<0:
        kmin = 0
    if kmax>N:
        kmax = N
    for i in range(kmin,kmax,1):
        print([i,freq_Hz[i],Xkmagn[i],Xk[i]])
else:
    print('Revisar: L debe ser menor o igual a N')
    print('L:',L,'; N:',N)

# GRAFICA
if N>=L:
    LN_texto= ', L='+str(L)+', N='+str(N)
    zp_texto = 'zero padding:'+str(N-L)
    plt.subplot(311)
    plt.stem(ki[0:len(xn)],np.real(xn),label='x[n]',
             markerfmt ='C2.',linefmt='C2-')
    if N>L:
        kj = np.arange(L,N,dt)
        zp = np.zeros(N-L)
        plt.stem(kj,zp,label = 'zero padding',
                 markerfmt ='C3.')
    plt.xlabel('ki')
    
    plt.title('x[n] vs |x[k]|'+LN_texto+', '+zp_texto)
    plt.legend()

    plt.subplot(312)
    plt.stem(ki[0:mitad], Xkmagn[0:mitad],
             label='X[k], k_=[0:N/2]',
             markerfmt ='C0.', linefmt='C0-')
    plt.stem(ki[mitad:], Xkmagn[mitad:],
             label='X[k], k_=[N/2:]',
             markerfmt ='C1.',linefmt='C1-')
    plt.stem(N,0,markerfmt ='C3.')
    plt.xlabel('ki_')
    plt.legend()

    plt.subplot(313)
    plt.stem(ki_shift[0:mitad], Xkmagn[0:mitad],
             label='H[k] centrada]',
             markerfmt ='C0.',linefmt='C0-')
    plt.stem(ki_shift[mitad:], Xkmagn[mitad:],
             markerfmt ='C1.',linefmt='C1-')
    plt.stem(mitad,0,markerfmt ='C3.')
    plt.xlabel('ki')
    plt.legend()
    plt.legend()
    plt.tight_layout()
    plt.show()

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]
..


5. DFT de una señal Real tipo Coseno

Referencia: McClellan 8.2.5 p316

Considere la señal de entrada x[n] coseno, de frecuencia 0.2 de forma discreta para L=50 tramos en tiempo en un intervalo de dos periodos. Para la DFT considere el caso que N=L, no se requiere relleno de ceros.

x[n] = 5 cos(2\pi(0.1)n]

n = 0, 1, ..., L-1

Una señal senoidal se puede escribir como la suma de exponenciales complejos y usar la propiedad de linealidad para encontrar el resultado.
El resultado de la DFT muestra la característica de simetría donde ocurre un máximo (pico), en este caso en k=k0=1 y k=N-k0=50-1=49. Las correspondientes frecuencias en Hertz son -0.1 y 0.1. La altura de los picos son aproximadamente AL/2 = 5(50)/2 = 125.

fft seno L=50 N=50Resultados numéricos con el algoritmo:

k0: 1 ; i: 26
freq_Hz: 0.1 ; Xk[k0]: (125+1.509903313490213e-14j)
[ i, freq[ki],|X[k]| ,X[k]]
[0, 0.0, 9.492406860545088e-15, (9.492406860545088e-15+0j)]
[1, 0.1, 125.0, (125+1.509903313490213e-14j)]
[2, 0.2, 4.842919552269065e-15, (-4.774353709997803e-15-8.120446056592065e-16j)]
[3, 0.30000000000000004, 3.4364294309926203e-15, (2.3207649852950894e-15-2.5343829855056534e-15j)]
[4, 0.4, 2.032850942959038e-15, (8.108973628378316e-16+1.8641159897474457e-15j)]

Para el caso que L<N, el valor de N se aumenta a 8*L, N=400, el resultado del pico de frecuencia se mantiene respecto al caso anterior.

fft seno L=50 N=400los resulttados numéricos con el algoritmo son:

k0: 8 ; i: 208
freq_Hz: 0.1 ; Xk[k0]: (125.00000000000001+1.554312234475219e-14j)
[ i, freq[ki],|X[k]| ,X[k]]
[4, 0.05, 53.33891994969286, (4.999999999999986+53.10405240092081j)]
[5, 0.0625, 75.60689606012141, (33.14721982316793+67.95339984018891j)]
[6, 0.07500000000000001, 96.58278363275512, (70.7485680957908+65.74856809579083j)]
[7, 0.08750000000000001, 113.74232040562568, (105.80156476944595+41.75337523593449j)]
[8, 0.1, 125.00000000000001, (125.00000000000001+1.554312234475219e-14j)]
[9, 0.1125, 128.94638956201757, (119.85004661115146-47.572446945521044j)]
[10, 0.125, 125.00127087574828, (90.85388423989122-85.85388423989122j)]
[11, 0.1375, 113.46542771722382, (47.653106752530654-102.97370879930693j)]

Instrucciones en Python

# ejemplo 8.7 p318 DFT zero-padding cos()
# telg1034 DSP fiec-espol edelros@espol.edu.ec
import numpy as np
import matplotlib.pyplot as plt

# INGRESO
L = 50   # Tramos en un periodo

# señal es rectangular periódico rect[n]
f0 = 0.1  # frecuencia fundamental de señal
xt = lambda t: 5*np.cos(2*np.pi*f0*t)

# para DFT
N = 8*L # N par mayor que L, tramos DFT

# PROCEDIMIENTO
# muestreo de señal x[n]
t0 = 0 ; tn = 1*(1/f0) # intervalo [t0,tn]
dt = (tn-t0)/L
ti = np.arange(t0,tn,dt)
xn = xt(ti)

# verificar que L<=N
if L<=N:
    # DFT transformada rapida de Fourier
    Xk = np.fft.fft(xn,n=N)
    Xkmagn = np.abs(Xk) # para gráfica
    Xkfase = np.angle(Xk)
    freq_Hz = np.fft.fftfreq(N,dt) # frecuencias en Hz

    mitad = int(N/2)
    k0 = np.argmax(Xk) # Pico |X[k]|

# SALIDA
if N>=L:
    print('k0:',k0,'; i:',mitad+k0)
    print('freq_Hz:',freq_Hz[k0],
          '; Xk[k0]:',Xk[k0])
    print('[ i, freq[ki],|X[k]| ,X[k]]')
    kmin = k0-4 ; kmax = k0+4
    if kmin<0:
        kmin = 0
    if kmax>N:
        kmax = N
    for i in range(kmin,kmax,1):
        print([i,freq_Hz[i],Xkmagn[i],Xk[i]])
else:
    print('Revisar: L debe ser menor o igual a N')
    print('L:',L,'; N:',N)

# GRAFICA
if N>=L:
    LN_texto= ', L='+str(L)+', N='+str(N)
    zp_texto = 'zero padding:'+str(N-L)
    plt.subplot(211)
    plt.stem(ti,np.real(xn),label='Re(x[n])',
             markerfmt ='C2.',linefmt='C2-')
    plt.stem(ti,np.imag(xn),label='Im(x[n])',
             markerfmt ='C4.',linefmt='C4:')
    if N>L:
        tj = np.arange(L*dt,N*dt,dt)
        zp = np.zeros(N-L)
        plt.stem(tj,zp,label = 'zero padding',
                 markerfmt ='C3.')
    plt.stem(L*dt,0,markerfmt ='C4.')
    plt.xlabel('ti (s)')
    
    plt.title('x[n] vs |x[k]|'+LN_texto+', '+zp_texto)
    plt.legend()

    plt.subplot(212)
    plt.stem(freq_Hz[0:mitad], Xkmagn[0:mitad],
             label='|X[k]| centrada]',
             markerfmt ='C0.',linefmt='C0-')
    plt.stem(freq_Hz[mitad:], Xkmagn[mitad:],
             markerfmt ='C1.',linefmt='C1-')
    plt.xlabel('freq (Hz)')
    plt.legend()
    plt.legend()
    plt.tight_layout()
    plt.show()

DFT: [ periódicas ] [ freq negativas ] [ simetría conjugada]  [ zero_padding ] [ ejemplo_cos ]