Filtros recursivos, o de respuesta al impulso infinita (IIR)

../_images/logo_UTN.svg

Por Mariano Llamedo Soria

Resumen

En este notebook se ejemplifica el diseño de filtros IIR mediante las funciones de síntesis digital directa de scipy.signal.

Se inicializa el entorno de simulación con los módulos utilizados.

import sympy as sp
import numpy as np
import scipy.signal as sig
import matplotlib.pyplot as plt

# Gráficos interactivos
#%matplotlib ipympl
# Gráficos estáticos
#%matplotlib inline

from pytc2.sistemas_lineales import plot_plantilla

Diseño digital directo

Dejando atrás el método de la transformada bilineal, pasamos a los métodos directos, donde las funciones de diseño operan directamente sobre una plantilla completamente digital. Esto simplifica enormemente el diseño, como se mostrará en los siguientes ejemplos, a partir de la plantilla que usamos en el notebook anterior:

# Tipo de aproximación.
        
aprox_name = 'butter'
# aprox_name = 'cheby1'
# aprox_name = 'cheby2'
# aprox_name = 'ellip'

# Por qué no hay bessel ?
#aprox_name = 'bessel'

# Requerimientos de plantilla

filter_type = 'lowpass'
# filter_type = 'highpass'
# filter_type = 'bandpass'
# filter_type = 'bandstop'


# plantillas normalizadas a Nyquist y en dB

if filter_type == 'lowpass':

    # fpass = 1/2/np.pi # 
    fpass = 0.25 # 
    ripple = 0.5 # dB
    fstop = 0.6 # Hz
    attenuation = 40 # dB

elif filter_type == 'highpass':

    fpass = 0.6 
    ripple = 0.5 # dB
    fstop = 0.25
    attenuation = 40 # dB

elif filter_type == 'bandpass':

    fpass = np.array( [0.4, 0.6] ) 
    ripple = 0.5 # dB
    fstop = np.array( [0.25, 0.75] ) 
    attenuation = 40 # dB
    
else:

    # bandstop
    fpass = np.array( [0.25, 0.75] ) 
    ripple = 0.5 # dB
    fstop = np.array( [0.4, 0.6] ) 
    attenuation = 40 # dB


    # Cálculo del filtro

# frecuencia de muestreo normalizada (Nyquist = 1)
fs = 2

if aprox_name == 'butter':

    order, wcutof = sig.buttord( 2*np.pi*fpass*fs/2, 2*np.pi*fstop*fs/2, ripple, attenuation, analog=True)
    orderz, wcutofz = sig.buttord( fpass, fstop, ripple, attenuation, analog=False)

elif aprox_name == 'cheby1':

    order, wcutof = sig.cheb1ord( 2*np.pi*fpass*fs/2, 2*np.pi*fstop*fs/2, ripple, attenuation, analog=True)
    orderz, wcutofz = sig.cheb1ord( fpass, fstop, ripple, attenuation, analog=False)
    
elif aprox_name == 'cheby2':

    order, wcutof = sig.cheb2ord( 2*np.pi*fpass*fs/2, 2*np.pi*fstop*fs/2, ripple, attenuation, analog=True)
    orderz, wcutofz = sig.cheb2ord( fpass, fstop, ripple, attenuation, analog=False)
    
elif aprox_name == 'ellip':
   
    order, wcutof = sig.ellipord( 2*np.pi*fpass*fs/2, 2*np.pi*fstop*fs/2, ripple, attenuation, analog=True)
    orderz, wcutofz = sig.ellipord( fpass, fstop, ripple, attenuation, analog=False)


# Diseño del filtro analógico

num, den = sig.iirfilter(order, wcutof, rp=ripple, rs=attenuation, btype=filter_type, analog=True, ftype=aprox_name)

my_analog_filter = sig.TransferFunction(num,den)
my_analog_filter_desc = aprox_name + '_ord_' + str(order) + '_analog'

# Diseño del filtro digital

numz, denz = sig.iirfilter(orderz, wcutofz, rp=ripple, rs=attenuation, btype=filter_type, analog=False, ftype=aprox_name)

my_digital_filter = sig.TransferFunction(numz, denz, dt=1/fs)
my_digital_filter_desc = aprox_name + '_ord_' + str(orderz) + '_digital'



# Plantilla de diseño

plt.figure(1)
plt.cla()

npoints = 1000
w_nyq = 2*np.pi*fs/2

w, mag, _ = my_analog_filter.bode(npoints)
plt.plot(w/w_nyq, mag, label=my_analog_filter_desc)

w, mag, _ = my_digital_filter.bode(npoints)
plt.plot(w/w_nyq, mag, label=my_digital_filter_desc)

plt.title('Plantilla de diseño')
plt.xlabel('Frecuencia normalizada a Nyq [#]')
plt.ylabel('Amplitud [dB]')
plt.grid(which='both', axis='both')

plt.gca().set_xlim([0, 1])

plot_plantilla(filter_type = filter_type , fpass = fpass, ripple = ripple , fstop = fstop, attenuation = attenuation, fs = fs)

plt.legend()
<matplotlib.legend.Legend at 0x7d35a34a1420>
../_images/8e1829e894972c2107b012271d5159545ee122aacf792cfd83d7bf8f4d254e50.png

En el ejemplo anterior, se observa que la función de diseño digital (y analógico) opera directamente sobre la plantilla y calcula el orden y la frecuencia de corte del filtro, para luego proceder directamente al cálculo de los coeficientes de la función que dan lugar al filtro. Esto se realiza con dos funciones, y como se verá más adelante, puede simplificarse a una sola función.

Sin embargo, el diseño ya no es iterativo, se obtiene el filtro deseado en un solo paso, y como ya se mostró con anterioridad, el filtro digital es de menor orden que el analógico para esta plantilla en concoreto.

Se verá en el ejemplo final, cómo se simplifica al máximo el diseño de filtros mediante la función iirdesign de scipy.signal.

# re-Diseño del filtro analógico
num, den = sig.iirdesign(wp = fpass *2*np.pi*fs/2,
                         ws = fstop *2*np.pi*fs/2,
                         gpass = ripple,
                         gstop = attenuation,
                         ftype=aprox_name,
                         output = 'ba',
                         analog=True)

my_analog_filter = sig.TransferFunction(num,den)
my_analog_filter_desc = aprox_name + '_ord_' + str(order) + '_analog'


# re-Diseño del filtro digital
numz, denz = sig.iirdesign(wp = fpass,
                         ws = fstop,
                         gpass = ripple,
                         gstop = attenuation,
                         ftype=aprox_name,
                         output = 'ba',
                         analog=False,
                         fs = fs)

Se observa cómo se simplifica enormemente el diseño de los filtros con los métodos directos. El siguiente paso será el análisis de sus respuestas en frecuencia.

my_digital_filter = sig.TransferFunction(numz, denz, dt=1/fs)
my_digital_filter_desc = aprox_name + '_ord_' + str(orderz) + '_digital'


# Plantilla de diseño

plt.figure(1)
plt.cla()

npoints = 1000
w_nyq = 2*np.pi*fs/2

w, mag, _ = my_analog_filter.bode(npoints)
plt.plot(w/w_nyq, mag, label=my_analog_filter_desc)

w, mag, _ = my_digital_filter.bode(npoints)
plt.plot(w/w_nyq, mag, label=my_digital_filter_desc)

plt.title('Plantilla de diseño')
plt.xlabel('Frecuencia normalizada a Nyq [#]')
plt.ylabel('Amplitud [dB]')
plt.grid(which='both', axis='both')

plt.gca().set_xlim([0, 1])

plot_plantilla(filter_type = filter_type , fpass = fpass, ripple = ripple , fstop = fstop, attenuation = attenuation, fs = fs)
plt.legend()
<matplotlib.legend.Legend at 0x7d35a3216590>
../_images/8e1829e894972c2107b012271d5159545ee122aacf792cfd83d7bf8f4d254e50.png

El resultado es exactamente el mismo que con los métodos anteriores, con algunas ventajas. En caso que la plantilla de diseño sea más exigente, y en consecuencia el filtro sea de mayor orden, se puede pedir que el filtro se implemente como una cascada de secciones de segundo orden (SOS).

# Diseño del filtro digital más exigente
sos_filter = sig.iirdesign(  wp = fpass,
                             ws = fpass+0.05,
                             gpass = ripple,
                             gstop = attenuation,
                             ftype=aprox_name,
                             output = 'sos',
                             analog=False,
                             fs = fs)

otra_desc = 'SOS - {:s} - orden:{:d}'.format(aprox_name, sos_filter.shape[0]*2)


# Plantilla de diseño

fig = plt.figure(1)
plt.cla()

npoints = 1000

w, hh = sig.sosfreqz(sos_filter, worN=npoints)
plt.plot(w/np.pi, 20*np.log10(np.abs(hh)), label=otra_desc)

# filtro anterior, como referencia
w, mag, _ = my_digital_filter.bode(npoints)
plt.plot(w/w_nyq, mag, label=my_digital_filter_desc)

plt.title('Plantilla de diseño')
plt.xlabel('Frecuencia normalizada a Nyq [#]')
plt.ylabel('Amplitud [dB]')
plt.grid(which='both', axis='both')

ax = plt.gca()
ax.set_xlim([0, 1])
ax.set_ylim([-60, 1])

plot_plantilla(filter_type = filter_type , fpass = fpass, ripple = ripple , fstop = fstop, attenuation = attenuation, fs = fs)
plt.legend()
plt.show()
../_images/987a10488142aca1d2bdd59dd7723b2d64ef49048cf0fbeec963737b67b38764.png

Ahora revisamos en detalle la banda de paso

ax.set_xlim([0, np.mean((fpass,fstop))])
ax.set_ylim([-2*ripple, np.mean((-ripple,1)) ])
fig
../_images/f106b0e4c59d468ef485fb9d1657a8a2e1fc548c27c76b4585676cd34c09df2f.png