2.3 Reconstrucción, sobre-muestreo, sub-muestreo y Nyquist

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]
..

1. Muestreo y Reconstrucción

La salida de un convertidor contínuo a discreto C-D es una señal discreta en tiempo con un número infinito de alias. Al observar un gráfico de espectro de frecuencias ω es necesario recordar que existen más señales fuera del intervalo mostrado.

Considere una señal x(t) simple, de una sola frecuencia, cuyo espectro contiene dos líneas a ±ω0 con amplitudes de (1/2 A e±ω0)

x(t) = A cos\Big(\omega_0 t + \varphi \Big) x[n] = x(n/f_s) A cos((\omega_0 /f_s)*n + \varphi = \frac{1}{2}A e^{j\varphi}e^{j(\omega_0/f_s)n} + \frac{1}{2}A e^{-j\varphi}e^{j(-\omega_0/f_s)n}

también tiene dos frecuencias ω en ±ω0/fs, con todos los alias en
±ω0/fs + 2πk, siendo k= ±1, ±2, ±3, …

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]
..


2. Sobre-muestreo

Referencia: McClellan ejercicio 4.5 p133

Realice el espectro contínuo de la señal x(t), la señal muestreada a fs=500 y el espectro discreto resultante. Observe que la frecuencia de muestreo es superior a la frecuencia de la señal x(t) para evitar el problema de aliasing. El proceso se conoce como sobre-muestreo. Para el ejercicio se usa una frecuencia 2.5 veces la recomendada por Nyquist.

x(t) = cos\Big(2\pi (100) t + \pi /3 \Big)

espectro n coseno sobremuestreo

Los convertidores D-C trasforman el espectro discreto en tiempo a una salida de espectro contínuo en el tiempo, seleccionando solo un par de las líneas de todas las posibilidades mostradas.

Para ser consistentes con la operación Digital hacia Analógico (D-A) se tomará siempre la frecuencia mas baja de cada grupo de alias o alias principal con |ω|<π.
El resultado siempre se encontrará entre [-fs/2, +fs/2].

En resumen, en sobre-muestreo la frecuencia original f0 es menor que fs/2 permite la reconstrucción mas exacta.

Para el análisis con el algoritmo se obtienen los siguientes parámetros para realizar la gráfica.

señal(t):   cos(100*t*(2*pi) + pi/3)
 espectro:
  freq : [-100.  100.]
  ampl : [1/2 1/2]
  fase : [-pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_max : 100.0
  freq_min : 100.0
  x_expand : cos(2*pi*n/5 + pi/3)

muestreo_tipo:  sobre-muestreo
muestreo: 11
 tamaño ki: 11 ; tamaño ti: 121
fs: 500  ; dt: 0.002  ; fs_veces: 12
x(t): cos(100*t*(2*pi) + pi/3)
x[n]: cos(n*(2*pi)/5 + pi/3)

señal[n]:   cos(n*(2*pi)/5 + pi/3)
 espectro_n:
  freq : [-2.4 -1.6 -0.4  0.4  1.6  2.4]
  ampl : [1/2 1/2 1/2 1/2 1/2 1/2]
  fase : [-pi/3 pi/3 -pi/3 pi/3 -pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_factor : pi
  freq_max : 2.4
  freq_min : 0.4
  BW : 2.0
  freq_alias : [ True  True False False  True  True]

señal reconstruida con alias principal:
cos(100*t*(2*pi) + pi/3)

Las instrucciones del bloque de ingreso para el algoritmo son:

# INGRESO
# usar np.pi para evitar frecuencia simplificada por sympy.
x = sym.cos(DosPi*100*t + pi/3)
x_etiqueta = 'x(t)' ; x_etiqueta_n = 'x[n]'

# señal muestreada
fs = 500  # muestreo discreto, freq_sampling
fs_veces = 12 # sobremuestreo para x(t)
muestrasN = 10 + 1  # para x[n]

aliasn = 1 # alias añadidos a x[n]
tolera = 1e-9 # tolerancia a error, números iguales

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]
..


3. Sub-muestreo y aliasing

Cuando fs < 2f0 la señal se encuentra sub-muestreada y se produce el aliasing. Para el ejercicio anterior, si el ejercicio lo realiza con fs = 80Hz, siendo f0=100 Hz, la tasa de muestreo es menor que la frecuencia de Nyquist y se presenta la señal de alias.

espectro n coseno submuestreo

las instrucciones para el bloque de ingreso del algoritmo son las mismas que en el ejercicio anterior, excepto que fs = 80 Hz

# señal muestreada
fs = 80  # muestreo discreto, freq_sampling

el resultado con el algoritmo es una señal reconstruida de tan solo 20Hz

x_0(t) = cos\Big(2\pi (20) t + \pi /3 \Big)
señal(t):   cos(100*t*(2*pi) + pi/3)
 espectro:
  freq : [-100.  100.]
  ampl : [1/2 1/2]
  fase : [-pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_max : 100.0
  freq_min : 100.0
  x_expand : cos(5*pi*n/2 + pi/3)

muestreo_tipo:  sub-muestreo
muestreo: 11
 tamaño ki: 11 ; tamaño ti: 121
fs: 80  ; dt: 0.0125  ; fs_veces: 12
x(t): cos(100*t*(2*pi) + pi/3)
x[n]: cos(5*n*(2*pi)/4 + pi/3)

señal[n]:   cos(5*n*(2*pi)/4 + pi/3)
 espectro_n:
  freq : [-4.5 -2.5 -0.5  0.5  2.5  4.5]
  ampl : [1/2 1/2 1/2 1/2 1/2 1/2]
  fase : [-pi/3 -pi/3 -pi/3 pi/3 pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_factor : pi
  freq_max : 4.5
  freq_min : 0.5
  BW : 4.0
  freq_alias : [ True  True False False  True  True]

señal reconstruida con alias principal:
cos(20*t*(2*pi) + pi/3)

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]
..


4. Folding o plegado de señal por sub-muestreo

Para una señal x(t) de 100 Hz se aplica muestreo insuficiente en el intervalo [f0,2f0] se presenta aliasing denominado «folding» o plegado de señal. Para el ejercicio de prueba y con fs = 125, se tiene:

x(t) = cos\Big(2\pi (100) t + \pi /3 \Big)

espectro n coseno folding
Los componentes de frecuencia entre ±π también son ±0.4π, sin embargo  el componente en +0.4π es un alias del componente de la frecuencia negativa -1.6π, que genera el «plegado» o «folding». La reconstrucción de la señal se realiza a f = 0.4(fs/2π) = fs/5 = 25 Hz. También se observa una inversión de fase. El resultado es semejante a una señal muestreada de 25 Hz con inversión de fase.

señal(t):   cos(100*t*(2*pi) + pi/3)
 espectro:
  freq : [-100.  100.]
  ampl : [1/2 1/2]
  fase : [-pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_max : 100.0
  freq_min : 100.0
  x_expand : cos(8*pi*n/5 + pi/3)

muestreo_tipo:  sub-muestreo y folding
muestreo: 11
 tamaño ki: 11 ; tamaño ti: 121
fs: 125  ; dt: 0.008  ; fs_veces: 12
x(t): cos(100*t*(2*pi) + pi/3)
x[n]: cos(4*n*(2*pi)/5 + pi/3)

señal[n]:   cos(4*n*(2*pi)/5 + pi/3)
 espectro_n:
  freq : [-3.6 -1.6 -0.4  0.4  1.6  3.6]
  ampl : [1/2 1/2 1/2 1/2 1/2 1/2]
  fase : [-pi/3 -pi/3 pi/3 -pi/3 pi/3 pi/3]
  etiq : ['$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(- \\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$'
 '$\\frac{1}{2}$$ e^j(\\frac{\\pi}{3})$']
  freq_factor : pi
  freq_max : 3.6
  freq_min : 0.3999999999999999
  BW : 3.2
  freq_alias : [ True  True False False  True  True]

señal reconstruida con alias principal:
cos(25.0*t*(2*pi) - (pi/3))

Otro caso a observar es cuando fs=100Hz, el resultado es una constante, pues siempre se obtiene la muestra de la señal con la misma magnitud.

espectro n coseno folding02

5. Algoritmo en Python

El algoritmo contiene componentes desarrollados en secciones anteriores. Para el espectro de frecuencias en dominio del tiempo se basa en lo descrito en Espectro – Operaciones en dominio de tiempo y frecuencia.

El desarrollo del muestreo usando la señal x(t) y fs toma como referencia lo descrito en Muestreo y aliasing.

La construcción del espectro discreto a partir de x[n] usa lo desarrollado en Espectro x[n] – Señal discreta en tiempo

# ejercicio 4.5 p133 Muestreo y Reconstrucción de señales sinusoidales
# telg1034 DSP fiec-espol edelros@espol.edu.ec
import numpy as np
import matplotlib.pyplot as plt
import sympy as sym
import telg1034 as dsp

# variables continuas
from telg1034 import t,A,w,f,p,pi,DosPi,I,equivalentes
# variables discretas
from telg1034 import n

# INGRESO
# usar np.pi para evitar frecuencia simplificada por sympy.
x = sym.cos(DosPi*100*t + pi/3)
x_etiqueta = 'x(t)' ; x_etiqueta_n = 'x[n]'

# señal muestreada
fs = 500  # muestreo discreto, freq_sampling
fs_veces = 12 # sobremuestreo para x(t)
muestrasN = 10 + 1  # para x[n]

aliasn = 1 # alias añadidos a x[n]
tolera = 1e-9 # tolerancia a error, números iguales

# PROCEDIMIENTO ----------------
# operaciones para espectro x(t)
# Ref: Blog|Espectro-Operaciones en dominio de tiempo y frecuencia
x_term = dsp.x_list_term_Add(x)
Xe_term = dsp.cos_to_euler_one_term(x_term)
x_term_expand = dsp.euler_to_cos_list(Xe_term)
un_espectro = dsp.cos_spectrum_list(x_term)
# espectro de cada señal
freq = un_espectro['freq']
ampl = un_espectro['ampl']
fase = un_espectro['fase']
etiqueta = un_espectro['etiq']

# freq_max y freq_min
freq_max = float(max(freq))
if len(freq[freq>0])>0:
    freq_min = float(min(freq[freq>0]))
else:
    freq_min = 0
un_espectro['freq_max'] = freq_max
un_espectro['freq_min'] = freq_min

# revisa muestreo Nyquist
if freq_max<=(fs/2):
    muestreo_tipo = 'sobre-muestreo'
if freq_max>(fs):
    muestreo_tipo = 'sub-muestreo'
if freq_max>(fs/2) and freq_max<=fs:
    muestreo_tipo = 'sub-muestreo y folding'
    
# Muestreo de señales sinusoidales
# Ref: blog|2.1 Muestreo y aliasing
xn = x.subs(t,n/fs)
# muestreo x[n]
dt = 1/fs 
ki = np.arange(0,muestrasN,1,dtype=int)
# muestreo x(t)
dtc = dt/fs_veces # para x(t)
ti = np.arange(0,ki[muestrasN-1]*dt + dtc, dtc)
tki = ki*dt

xt = sym.lambdify(t,x, modules=equivalentes)
xki = xt(tki) 
xti = xt(ti)

# operaciones para espectro x[n]
# Ref: blog|2.2 Espectro x[n] – Señal discreta en tiempo
x_term = dsp.x_list_term_Add(xn)
Xe_term = dsp.cos_to_euler_one_term(x_term)
x_term_expand = dsp.euler_to_cos_list(Xe_term)
un_espectro_n = dsp.cos_spectrum_list(x_term)
# espectro de cada señal
freq_n = np.array(un_espectro_n['freq'])
ampl_n = np.array(un_espectro_n['ampl'])
fase_n = np.array(un_espectro_n['fase'])
etiq_n = np.array(un_espectro_n['etiq'])
un_espectro['x_expand'] = x_term_expand

# Añadir alias a señal
freqn_conteo = len(freq_n)
freqn_alias = np.zeros(freqn_conteo,dtype=bool)
for i in range(0,freqn_conteo,1):
    freq_i = freq_n[i]
    ampl_i = ampl_n[i]
    fase_i = fase_n[i]
    etiq_i = etiq_n[i]
    for k in range(1,aliasn+1,1):
        # añade freqm[i] + 2*pi*k
        freq_n = np.concatenate([freq_n,[freq_i+2*pi*k]])
        ampl_n = np.concatenate([ampl_n,[ampl_i]])
        fase_n = np.concatenate([fase_n,[fase_i]])
        etiq_n = np.concatenate([etiq_n,[etiq_i]])
        freqn_alias = np.concatenate([freqn_alias,[True]])
        # añade freqm[i] - 2*pi*k
        freq_n = np.concatenate([freq_n,[-(freq_i.evalf()+2*np.pi*k)]])
        ampl_n = np.concatenate([ampl_n,[ampl_i]])
        fase_n = np.concatenate([fase_n,[-fase_i]])
        texto = '$' + sym.latex(ampl_i)+'$'
        if fase_i!=sym.S.Zero:
            texto = texto+f'$ e^j('+sym.latex(fase_i)+')$'
        etiq_n = np.concatenate([etiq_n,[texto]])
        freqn_alias = np.concatenate([freqn_alias,[True]])
# ordena frecuencias
orden = np.argsort(freq_n)
freq_n = freq_n[orden]
ampl_n = ampl_n[orden]
fase_n = fase_n[orden]
etiq_n = etiq_n[orden]
freqn_alias = freqn_alias[orden]

# revisa factor pi en freq_n
unfactor = sym.S.One ; factor_pi = False
freqn_conteo = len(freq_n)
for i in range(0,freqn_conteo,1):
    if freq_n[i].has(pi):
        factor_pi = True
if factor_pi:
    freq_n = np.array(freq_n/pi,dtype=float)
    unfactor = pi
# actualiza espectro de señal con factor pi,alias,orden
un_espectro_n['freq_factor'] = unfactor
un_espectro_n['freq'] = freq_n
un_espectro_n['ampl'] = ampl_n
un_espectro_n['fase'] = fase_n
un_espectro_n['etiq'] = etiq_n

# ancho de banda, freq_max y freq_min
freq_posi = freq_n[freq_n>0]
freq_max_n = float(max(freq_posi))
freq_min_n = 0
if len(freq_posi)>0:
    freq_min_n = float(min(freq_posi))
freq_centro_n = (freq_max_n + freq_min_n)/2
un_espectro_n['freq_max'] = freq_max_n
un_espectro_n['freq_min'] = freq_min_n
un_espectro_n['BW'] = freq_max_n - freq_min_n
    
# aliasing, Revisa espectro de frecuencias  
freq_conteo = len(freq_n)
freq_alias = np.zeros(freq_conteo,dtype=bool)
for i in range (0,freq_conteo,1):
    unafreq = freq_n[i]
    k = 1
    unalias = unafreq + k*2
    while unalias<=freq_max_n:
        for j in range(i+1,freq_conteo,1): 
            if abs(unalias - freq_n[j])<tolera:
                if abs(freq_n[i])>abs(freq_n[j]):
                    freq_alias[i] = True
                else:
                    freq_alias[j] = True
        k = k+1
        unalias = unafreq + k*2
freq_alias_conteo = np.sum(freq_alias)
un_espectro_n['freq_alias'] = freq_alias

# reconstruye señal sin alias
freq_alias0 = np.invert(freq_alias)
freq_x0 = freq_n[freq_alias0]
ampl_x0 = ampl_n[freq_alias0]
fase_x0 = fase_n[freq_alias0]
x0_cuenta = len(freq_x0)
x0t = sym.S.Zero
for i in range(0,x0_cuenta,1):
    if freq_x0[i]>=0:
        una_freq = freq_x0[i]*unfactor*(fs/(2*pi))
        una_freq = dsp._float_is_int(una_freq) # si es entero
        x0t = x0t + 2*ampl_x0[i]*sym.cos(DosPi*una_freq*t+sym.UnevaluatedExpr(fase_x0[i]))        
x0tr = x0t.subs(pi,np.pi)
x0tr = sym.lambdify(t,x0tr,modules=equivalentes)
x0_ti = x0tr(ti)

# SALIDA
print('señal(t):  ',x)
print(' espectro:')
for parametro in un_espectro:
    print(' ',parametro,':',un_espectro[parametro])

print('\nmuestreo_tipo: ',muestreo_tipo)
print('muestreo:',muestrasN)
print(' tamaño ki:',len(ki),'; tamaño ti:',len(ti))
print('fs:',fs,' ; dt:',dt, ' ; fs_veces:',fs_veces)
print('x(t):',x)
print('x[n]:',xn)

print('\nseñal[n]:  ',xn)
print(' espectro_n:')
for parametro in un_espectro_n:
    print(' ',parametro,':',un_espectro_n[parametro])

print('\nseñal reconstruida con alias principal:')
print(x0t)

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]
..


6. Gráfica

Con los componentes descritos en la parte de algoritmos, se ajustan las gráficas de cada sección para cada sub-gráfica en la figura.

# GRAFICAS  ----------------------------------------
# Grafica de espectro de frecuencias
# Ref: blog|2.2 Espectro x[n] – Señal discreta en tiempo
graf_dx = 0.12
fig_espectro = plt.figure()
ampl_max = float(max(ampl))
freq_max = float(max(max(freq),fs))*(1+graf_dx/2)
graf_fasor = fig_espectro.add_subplot(311)
if freq_max!=0:
    graf_fasor.set_xlim([-freq_max*(1+graf_dx),
                         freq_max*(1+graf_dx)])
else:
    graf_fasor.set_xlim([-1,1])
graf_fasor.set_ylim([0,ampl_max*(1+graf_dx*2)])
graf_fasor.grid()
graf_fasor.axhline(0,color='black')
graf_fasor.axvline(0,linestyle='dotted',color='grey')
graf_fasor.stem(freq,ampl) # grafica magnitud
for k in range(0,len(freq),1): # etiquetas de fasor
    graf_fasor.annotate(etiqueta[k],xy=(freq[k],ampl[k]),
        xytext=(0,5),textcoords='offset points',ha='center')
# fs frecuencia de muestreo
graf_fasor.stem(fs,ampl_max/2,linefmt = 'C3:', ) # fs
graf_fasor.annotate('fs',xy=(fs,ampl_max/2),
        xytext=(0,5),textcoords='offset points',ha='center')
graf_fasor.set_ylabel('Magnitud '+x_etiqueta)
graf_fasor.set_xlabel('freq Hz', labelpad=0,)
texto = '$' + sym.latex(x)+'$' +'  ; fs='+str(fs)
texto = texto + '  ; alias='+str(aliasn)
graf_fasor.set_title(texto)

# GRAFICA x(t), x[n], x(t)_alias0
graf_xt = fig_espectro.add_subplot(312)
graf_xt.plot(ti,xti, color='gray',label='x(t)')
graf_xt.plot(ti,x0_ti, color='blue',label='x(t)_alias0',
             linestyle='dotted')
graf_xt.stem(tki,xki,linefmt = 'C0:',label='x[n]')
graf_xt.set_xlabel('t')
graf_xt.set_ylabel('amplitud')
graf_xt.legend()
graf_xt.grid()

# GRAFICAS de espectro de frecuencias discretas---------
# Ref: blog|2.2 Espectro x[n] – Señal discreta en tiempo
freq_n = un_espectro_n['freq']
ampl_n = un_espectro_n['ampl']
etiq_n = un_espectro_n['etiq']
freq_alias = un_espectro_n['freq_alias']
ampl_max = float(max(ampl_n))*(1+graf_dx*2)
freq_max = float(max(un_espectro_n['freq_max'],1))*(1+graf_dx/2)
# grafica
graf_fasorn = fig_espectro.add_subplot(313)
graf_fasorn.set_xlim([-freq_max,freq_max])
graf_fasorn.set_ylim([min([min(ampl_n),0]),ampl_max])
graf_fasorn.grid()
graf_fasorn.axhline(0,color='black')
graf_fasorn.axvline(0,linestyle='dotted',color='grey')
# grafica magnitud
freq_noalias = np.invert(freq_alias) 
graf_fasorn.stem(freq_n[freq_noalias],ampl_n[freq_noalias],
                 linefmt = 'C0--')
graf_fasorn.stem(freq_n[freq_alias],ampl_n[freq_alias],
                 linefmt = 'C1--',label = 'alias') 
if un_espectro_n['freq_factor'] != sym.S.One:
    unfactor = r'$'+sym.latex(un_espectro_n['freq_factor'])+'$'
    freq_etiq = []
    for un_num in freq_n:
        freq_etiq.append(f''+str(round(un_num,4))+unfactor)
    graf_fasorn.set_xticks(ticks=freq_n,labels=freq_etiq)
for k in range(0,len(freq_n),1): # etiquetas de fasor
    graf_fasorn.annotate(etiq_n[k],xy=(freq_n[k],ampl_n[k]),
                 xytext=(0,5),textcoords='offset points',
                 ha='center')
graf_fasorn.set_ylabel('Magnitud '+x_etiqueta_n)
graf_fasorn.set_xlabel('freq rad')
graf_fasorn.legend()

plt.tight_layout()
plt.show()

[ reconstrucción ] [ sobre-muestreo ] [ sub-muestreo ] [ algoritmo ] [ gráfica ]