2.5 D-to-C - Interpolación Zero-Order Hold o con pulso rectangular

[ interpola pulso ] [ pulso rectangular ] [ algoritmo ] [ gráfica interactiva ]
..


1. Interpolación con Pulsos

Referencia: McClellan 4-3.1 p141

Un  sistema que convierte una señal y[n] en forma Discreta a la forma Contínua y(t), conocido como D-to-C,  usa interpolación para rellenar los espacios entre muestras. En el proceso se deben considerar efectos como el "aliasing" y "folding".

La implementación del concepto D-to-C en un sistema físico o "hardware" se los conoce como Convertidor Digital-Analógico (DAC) que completa los espacios entre muestras con un pulso p(t)

y(t) = \sum_{n=-\infty}^{\infty} y[n] p(t-nTs)

[ interpola pulso ] [ pulso rectangular ] [ algoritmo ] [ gráfica interactiva ]

..


2. Interpolación con Pulso rectangular, Zero-Order Hold

Un pulso simple, simétrico es un pulso rectangular de la forma:

p(t) = \begin{cases} 1 & -\frac{1}{2} T_s \lt t \leq \frac{1}{2} T_s \\ 0 & otro caso \end{cases}

Pulso Rectangular 01

Para una señal senoidad de frecuencia 83Hz y amplitud unitaria se obtienen las muestras x[n]. La frecuencia de muestreo fs es de 200Hz.  El proceso de re-construcción de x(t) llena los espacios entre muestras usando el pulso descrito en p(t). En éste caso p(t) es un pulso rectangular.

Los valores de las muestras x[n[ se presentan marcados con puntos para como una referencia en la gráfica.

Interpola Pulso Rectangular animate

Si consideramos un muestreo de señal a frecuencia fs=200Hz, se tiene que el pulso tiene una duración Ts = 5 ms y amplitud 1. Cada término de x[n]p(t-nTs) crea una sección plana de amplitud x[n] centrada en nTs, como se muestra en la gráfica.

2.1 Algoritmo en Python para un pulso

# D-to-C, Interpolacion con pulso rectangular
# ejemplo 4.3.2 p142
import numpy as np
import matplotlib.pyplot as plt

# INGRESO
fs = 200  # Hz muestreo
fs_veces = 16 # suavizar x(t), sobremuestreo
titulo = 'pulso rectangular'
nT = 2  # graficar periodos de la señal

# pulso rectangular
pulso_causal = True # No causal, pulso centrado en cero
pulso_ancho = 1
dutyc = 1 # entre [0,1] duty_cycle

# PROCEDIMIENTO
Ts = 1/fs
dtc = Ts/fs_veces # suavizar x(t), sobremuestreo

#https://blog.espol.edu.ec/telg1001/senales-escalon-%ce%bct-e-impulso-%ce%b4t/
#u = lambda t: np.piecewise(t,t>=0,[1,0])
u = lambda t: np.heaviside(t,1)
# pulso rectangular
#dutyc = 1 # entre [0,1] duty_cycle
u1 = lambda t: u(t+Ts*dutyc/2)
u2 = lambda t: u(t-Ts*dutyc/2)
rectangular = lambda t: u1(t) - u2(t)             

# x[n] interpolacion en Ts, a muestreo fs
t_unpulso = np.arange(-Ts/2*nT,Ts/2*nT+dtc,dtc)
unpulso = rectangular(t_unpulso)

# SALIDA
print('muestras:',len(t_unpulso))
print('t_unpulso:',t_unpulso)

# GRAFICA de un pulso
plt.axhline(0,color='Black')
plt.axvline(-Ts,color='red',linestyle='dotted')
plt.axvline(Ts,color='red',linestyle='dotted')

# p(t) componentes
plt.plot(t_unpulso,unpulso, color='green',linestyle='dashed',
         label='p_rect(t)')
plt.stem([0],[1],linefmt = 'C1:', label='xn[0]')

# grafica entorno
plt.xlabel('t')
plt.ylabel('amplitud')
texto = titulo + ' ; fs='+str(fs)
texto = texto +' ; Ts='+str(Ts)
plt.title(texto)
plt.grid()
plt.legend()
plt.tight_layout()
plt.show()

[ interpola pulso ] [ pulso rectangular ] [ algoritmo ] [ gráfica interactiva ]
..


3. Algoritmo en Python Interpola con pulso rectangular

El algoritmo se divide en:
- cálculo de x(t)
- cálculo de x[n]
- reconstrucción x(t) a partir de x[n] usando pulsos
- gráfica x(t)
- gráfica de x[n]

interpola Pulso Rectangular 01

La señal x(t) se convierte a discreta usando una tasa de muestras fs. Para observar la señal x(t), se indica el número de periodos nT de la frecuencia f0. Como ejemplo en la gráfica se realizan nT=2.3 periodos de la señal cosenoidal.

La señal de x(t) se suaviza visualmente al usar el factor fs_veces como sobre-muestreo respecto a fs. En este caso se usa fs=16. lo que genera muestras de señal cada Ts/fs_veces denominado en el algoritmo como tamaño de paso dtc.

Para el relleno de los espacios entre muestras x[n] se usa un modelo de pulso rectangular de tamaño Ts=1/fs que se copia cada vez que se avanza un espacio Ts,

Los valores de tiempo en la gráfica deben considerar la cantidad de muestras x[n], el ancho del pulso, el tiempo de inicio si es o no causal y la frecuencia de muestreo. El proceso se lo agrupa en la función pulso_ti(). El ancho del pulso cambia acorde a la forma del pulso a usar, triangular o Sinc(t). El pulso también se suaviza para la gráfica con fs_veces.

El proceso de reconstrucción de x(t) con pulsos consiste en repetir los valores del vector de un pulso en los tiempos que se dispone de una nueva muestra xki. En el caso que el ancho del pulso sea mayor que 1, los valores de los pulsos se traslapan y se requiere sumar los valores en un solo vector xn_pulsos . El efecto de ésta operación se detalla mejor con las señales triangular y Sinc(t) por tener un ancho de pulso mayor a 1.

Por el momento cada pulso se replica en cada muestra x[n] de la primera figura en la gráfica usando un color diferente. El resultado final de xn_pulsos se presenta en la segunda gráfica como x(t) pulsos.

# D-to-C, Interpolacion con pulso rectangular
# ejemplo 4.3.2 p142
import numpy as np
import matplotlib.pyplot as plt

# INGRESO
# señal x(t)
f0 = 83   # frecuencia de senal
fase0 = 0 # [0,2*np.pi]
xt = lambda t: np.cos(2*np.pi*f0*t + fase0)

fs = 200  # Hz muestreo
fs_veces = 16 # suavizar x(t), sobremuestreo
nT = 2.3  # graficar periodos de la señal

titulo = 'Interpola x(t) pulso rectangular '
# pulso rectangular
pulso_causal = True # No causal, pulso centrado en cero
pulso_ancho = 1
dutyc = 1 # entre [0,1] duty_cycle

casicero = 1e-10 # cero para valores menores

# PROCEDIMIENTO
Ts = 1/fs # tamaño de paso con fs
T =  1/f0 # periodo de señal
dtc = Ts/fs_veces # suavizar x(t), sobremuestreo

# muestreo x(t)
ti = np.arange(0,nT*T+dtc,dtc)
xti = xt(ti)
ti_max = max(ti)

# muestreo x[n]
muestras_n = 2
if ti_max>=Ts: # varias muestras
    muestras_n = int(ti_max/Ts)+1
ki  = np.arange(0,muestras_n,1,dtype=int)
tki = ki*Ts # muestras x[n]
xki = xt(tki)
tkj = tki   # x[n] alias0
ti_0 = ti   # No Causal, pulso centrado en cero
if pulso_causal: # Causal
    tkj = tki + Ts*pulso_ancho/2 
    ti_0 = ti + Ts*pulso_ancho/2
xti_0 = xti

def pulso_ti(muestras_n,fs,fs_veces,ancho=1,causal=True):
    ''' tiempos para las n muestras x(t) con pulsos a frecuencia fs.
    Para suavizar el pulso se usa fs_veces para el sobremuestreo.
    El ancho del pulso es veces el periodo de muestreo (1/Ts)
    Si es causal el tiempo inicia en t=0, centrado en cero.
    Si no es causal el tiempo inicia t en mitad de intervalo.
    '''
    Ts = 1/fs # muestreo periodo
    dtc = Ts/fs_veces # suavizar pulso(t), sobremuestreo
    t_Ts = np.arange(0,Ts,dtc) # tiempo en periodo Ts sobremuestreado
    
    mitad = Ts*ancho/2
    t_mitad = np.arange(0,mitad,dtc)
    t_pulsos = np.copy(t_mitad) # mitad de primer pulso
    for i in range(1,muestras_n,1):
        t_pulsos = np.concatenate((t_pulsos,t_Ts + t_pulsos[-1]+dtc))
    # mitad de último pulso para muestra_n
    t_pulsos = np.concatenate((t_pulsos,t_mitad + t_pulsos[-1]+dtc))

    # https://blog.espol.edu.ec/telg1001/sistemas-causales-y-no-causales/
    if causal == False: # centrado en cero
        t_pulsos = t_pulsos - mitad
    return(t_pulsos)

# x[n] interpolacion en Ts, a muestreo fs
t_pulsos = pulso_ti(muestras_n,fs,fs_veces,
                    pulso_ancho,pulso_causal)

# https://blog.espol.edu.ec/telg1001/senales-escalon-%ce%bct-e-impulso-%ce%b4t/
#u = lambda t: np.piecewise(t,t>=0,[1,0])
u = lambda t: np.heaviside(t,1)
# pulso rectangular entre [-(1/fs)/2,(1/fs)/2], no causal
u1 = lambda t: u(t+Ts*dutyc/2)
u2 = lambda t: u(t-Ts*dutyc/2)
rectangular = lambda t: u1(t) - u2(t)             

# unpulso muestreo
t_unpulso = pulso_ti(1,fs,fs_veces,
                     pulso_ancho,pulso_causal)
t_pulsoEval= t_unpulso
if pulso_causal:
    t_pulsoEval = t_unpulso-Ts*pulso_ancho/2
unpulso = rectangular(t_pulsoEval)
muestras_pulso = len(t_unpulso)

# reconstruye x(t) con pulsos
muestras_N = len(xki)
mitad_pulso = int(muestras_pulso/2)
pulsos_vacio = np.zeros(len(t_pulsos),dtype=float)
xn_pulsos = np.copy(pulsos_vacio)
xk_pulsos = []
for j in range(0,muestras_N,1): # x(t) pulsos
    k0 = int(j*fs_veces)
    kn = k0 + muestras_pulso
    pulsoj = np.copy(pulsos_vacio)
    pulsoj[k0:kn]= pulsoj[k0:kn] + unpulso*xki[j]
    xk_pulsos.append(pulsoj)
    xn_pulsos = xn_pulsos+pulsoj
    
# SALIDA
print('fs:',fs,'Hz ; Ts:',Ts,' s')
print('pulso_causal:',pulso_causal,' : dutycycle:',dutyc)
print('muestras_pulso:',muestras_pulso)
print('t_unpulso:',t_unpulso)
print('muestras_tiempo:',len(t_pulsos))

# GRAFICAS ----------------------------
fig, [graf_t,graf_n] = plt.subplots(2,1)

# x(t) grafico entorno
t_causal = 0
if pulso_causal:
    t_causal = Ts*pulso_ancho/2
graf_t.axhline(0,color='black')
graf_t.axvline(0,color='black')
graf_t.set_xlabel('t')
graf_t.set_ylabel('amplitud')
graf_t.set_xlim([t_pulsos[0]-t_causal,
                 t_pulsos[-1]-t_causal])
graf_t.grid()
texto = titulo + ' ; f0='+str(f0)
texto = texto + ' ; fs='+str(fs)
texto = texto + ' ; causal:'+str(pulso_causal)
graf_t.set_title(texto)
# x(t) componentes
graf_t.plot(ti,xti,label='x(t)')
graf_t.stem(tki,xki,label='x[n]',linefmt = 'C1:')
for i in range(0,muestras_N,1):
    graf_t.plot(t_pulsos-t_causal,xk_pulsos[i],
                linestyle='dashed')
graf_t.legend()

# x[n] grafico entorno
graf_n.axhline(0,color='black')
graf_n.axvline(0,color='black')
graf_n.set_xlabel('t')
graf_n.set_ylabel('amplitud')
graf_n.set_xlim([t_pulsos[0],t_pulsos[-1]])
graf_n.grid()
# x[n] componentes
graf_n.plot(ti_0,xti_0,linestyle='dotted',
            label='x(t)')
graf_n.stem(tkj,xki,label='x[n]',linefmt = 'C1:')
graf_n.plot(t_pulsos,xn_pulsos,
            label='x(t) pulso',
            lw=2, color='orange')

graf_n.legend()
plt.tight_layout()
plt.show()

[ interpola pulso ] [ pulso rectangular ] [ algoritmo ] [ gráfica interactiva ]
..


4. Algoritmo en Python para gráfico interactivo con fs

Para fines didácticos se añade un control en la gráfica para la frecuencia de muestreo fs, con lo que observa mejor el efecto que sub-muestreo, sobre-muestreo y el uso de 2 veces la frecuencia usada en la señal o Nyquist.

Interpola Pulso Rectangular animateCada cambio de fs tiene efectos sobre las muestras, el ancho de los pulsos y la reconstrucción de x(t). Por tal motivo se agrupan los cálculos de x(t) y x[n] en una función xt_actualiza(), y el proceso de reconstrucción y operaciones con x[n] como la función xn_actualiza().

# D-to-C, Interpolacion con pulso rectangular
# grafico interactivo
# ejemplo 4.3.2 p142
import numpy as np
import matplotlib.pyplot as plt

# INGRESO
# señal x(t)
f0 = 83   # frecuencia de senal
fase0 = 0 # [0,2*np.pi]
xt = lambda t: np.cos(2*np.pi*f0*t + fase0)

fs = 200  # Hz muestreo
fs_veces = 16 # suavizar x(t), sobremuestreo
nT = 2.3  # graficar periodos de la señal

titulo = 'Interpola x(t) pulso rectangular '
# pulso rectangular
pulso_causal = True # No causal, pulso centrado en cero
pulso_ancho = 1
dutyc = 1 # entre [0,1] duty_cycle

casicero = 1e-10 # cero para valores menores

# PROCEDIMIENTO
Ts = 1/fs # tamaño de paso con fs
T =  1/f0 # periodo de señal
dtc = Ts/fs_veces # suavizar x(t), sobremuestreo

def xt_actualiza(xt,f0,fs,fs_veces,nT,
                 pulso_ancho=1,pulso_causal=True):
    ''' x(t) muestreada a fs, para nT periodos de x(t).
    gráfica suavizada con fs_veces. No causal, pulso centrado en cero.
    Causal el pulso inicia en t=0.
    '''
    Ts = 1/fs # tamaño de paso con fs
    T =  1/f0 # periodo de señal
    dtc = Ts/fs_veces # suavizar x(t), sobremuestreo

    # muestreo x(t)
    ti = np.arange(0,nT*T+dtc,dtc)
    xti = xt(ti)
    ti_max = max(ti)

    # muestreo x[n]
    muestras_n = 2
    if ti_max>=Ts: # varias muestras
        muestras_n = int(ti_max/Ts)+1
    ki  = np.arange(0,muestras_n,1,dtype=int)
    tki = ki*Ts # muestras x[n]
    xki = xt(tki)
    tkj = tki   # x[n] alias0
    ti_0 = ti   # No Causal, pulso centrado en cero
    if pulso_causal: # Causal
        tkj = tki + Ts*pulso_ancho/2 
        ti_0 = ti + Ts*pulso_ancho/2
    xti_0 = xti
    return(ti,xti,tki,xki,tkj,ti_0,xti_0)

xt_list = xt_actualiza(xt,f0,fs,fs_veces,nT,
                       pulso_ancho,pulso_causal)
[ti,xti,tki,xki,tkj,ti_0,xti_0] = xt_list # x(t),x[n],x(t)_alias0
muestras_n = len(tki)

def pulso_ti(muestras_n,fs,fs_veces,ancho=1,causal=True):
    ''' tiempos para las n muestras x(t) con pulsos a frecuencia fs.
    Para suavizar el pulso se usa fs_veces para el sobremuestreo.
    El ancho del pulso es veces el periodo de muestreo (1/Ts)
    Si es causal el tiempo inicia en t=0, centrado en cero.
    Si no es causal el tiempo inicia t en mitad de intervalo.
    '''
    Ts = 1/fs # muestreo periodo
    dtc = Ts/fs_veces # suavizar pulso(t), sobremuestreo
    t_Ts = np.arange(0,Ts,dtc) # tiempo en periodo Ts sobremuestreado
    
    mitad = Ts*ancho/2
    t_mitad = np.arange(0,mitad,dtc)
    t_pulsos = np.copy(t_mitad) # mitad de primer pulso
    for i in range(1,muestras_n,1):
        t_pulsos = np.concatenate((t_pulsos,t_Ts + t_pulsos[-1]+dtc))
    # mitad de último pulso para muestra_n
    t_pulsos = np.concatenate((t_pulsos,t_mitad + t_pulsos[-1]+dtc))

    # https://blog.espol.edu.ec/telg1001/sistemas-causales-y-no-causales/
    if causal == False: # centrado en cero
        t_pulsos = t_pulsos - mitad
    return(t_pulsos)

def xn_actualiza(fs,fs_veces,xki,dutyc,
                 pulso_ancho=1,pulso_causal=True):
    ''' x(t) reconstruida con pulsos
    '''
    Ts = 1/fs # tamaño de paso con fs
    T =  1/f0 # periodo de señal
    dtc = Ts/fs_veces # suavizar x(t), sobremuestreo

    #https://blog.espol.edu.ec/telg1001/senales-escalon-e-impulso/
    #u = lambda t: np.piecewise(t,t>=0,[1,0])
    u = lambda t: np.heaviside(t,1)
    # pulso rectangular
    #dutyc = 1 # entre [0,1] duty_cycle
    u1 = lambda t: u(t+Ts*dutyc/2)
    u2 = lambda t: u(t-Ts*dutyc/2)
    rectangular = lambda t: u1(t) - u2(t)

    # x[n] interpolacion en Ts, a muestreo fs
    muestras_n = len(xki)
    t_pulsos = pulso_ti(muestras_n,fs,fs_veces,
                    pulso_ancho,pulso_causal)

    # unpulso muestreo
    t_unpulso = pulso_ti(1,fs,fs_veces,
                         pulso_ancho,pulso_causal)
    t_pulsoEval= t_unpulso
    if pulso_causal:
        t_pulsoEval = t_unpulso-Ts*pulso_ancho/2
    unpulso = rectangular(t_pulsoEval)
    muestras_pulso = len(t_unpulso)

    muestras_N = len(xki)
    pulsos_vacio = np.zeros(len(t_pulsos),dtype=float)
    xn_pulsos = np.copy(pulsos_vacio)
    xk_pulsos = []
    for j in range(0,muestras_N,1): # x(t) pulsos
        k0 = int(j*fs_veces)
        kn = k0 + muestras_pulso
        pulsoj = np.copy(pulsos_vacio)
        pulsoj[k0:kn]= pulsoj[k0:kn] + unpulso*xki[j]
        xk_pulsos.append(pulsoj)
        xn_pulsos = xn_pulsos+pulsoj

    return(t_pulsos,xn_pulsos,t_unpulso,unpulso)

xn_list = xn_actualiza(fs,fs_veces,xki,dutyc,
                       pulso_ancho,pulso_causal)
[t_pulsos,x_pulsos,t_unpulso,unpulso] = xn_list # x[n] pulsos, pulso

# SALIDA
print('fs:',fs,'Hz ; Ts:',Ts,' s')
print('pulso_causal:',pulso_causal,' : dutycycle:',dutyc)
print('muestras_pulso:',len(t_unpulso))
print('t_unpulso:',t_unpulso)
print('muestras_tiempo:',len(t_pulsos))

# GRAFICAS  interactivas----------------------------
from matplotlib.widgets import Slider, Button, TextBox
import telg1034 as dsp
graf_dx = 0.12 # margen en eje x

fig, [graf_t,graf_n] = plt.subplots(2,1)

# x(t) grafico entorno
graf_t.axhline(0,color='black')
graf_t.set_xlabel('t')
graf_t.grid()
texto = titulo + ' ; f0='+str(f0)
texto = texto +' ; fs='+str(fs)
graf_t.set_title(texto)
# x(t) componentes
linea_xt, = graf_t.plot(ti,xti,label='x(t)')
puntos_xn = graf_t.stem(tki,xki,label='x[n]',linefmt = 'C1:')
graf_t.set_xlim([t_pulsos[0],ti[-1]])
graf_t.legend()

# x[n] grafico entorno
graf_n.axhline(0,color='black')
graf_n.set_xlabel('t_n')
graf_n.grid()
# x[n] componentes
linea_xt0, = graf_n.plot(ti_0,xti_0,linestyle='dotted',
                         label='x(t)')
puntos_xki = graf_n.stem(tkj,xki,linefmt = 'C1:') #, label='x[n]')
linea_x_pulsos, = graf_n.plot(t_pulsos,x_pulsos,label='x(t) pulsos')
linea_pulso, = graf_n.plot(t_unpulso,unpulso,
                           label='pulso',linestyle='dashed',lw=2)
graf_n.set_xlim([t_pulsos[0],ti[-1]])
graf_n.legend()

plt.tight_layout()
# plt.show()

# grafica interactiva
plt.subplots_adjust(bottom=0.25) # espacio widgets

# slider: barras para valores 
# amplitud slider [x,y,ancho,alto]
fs_donde = plt.axes([0.2, 0.10, 0.65, 0.03])
df_pasos = 5
fs_slider = Slider(fs_donde, 'fs',
                   (f0//df_pasos)*df_pasos,
                   (max([fs,10*f0])+2*df_pasos),
                   valinit = fs, valstep = df_pasos,
                   orientation='horizontal')

def grafico_actualiza(val):
    # actualiza valores x,y
    fs = fs_slider.val
    
    xt_list = xt_actualiza(xt,f0,fs,fs_veces,nT,
                           pulso_ancho,pulso_causal)
    [ti,xti,tki,xki,tkj,ti_0,xti_0] = xt_list # x(t),x[n],x(t)_alias0
    dsp.stem_update(puntos_xn,tki,xki,graf_t) # x[n]
    dsp.stem_update(puntos_xki,tkj,xki,graf_n)
    linea_xt0.set_xdata(ti_0) #x(t)_alias0
    linea_xt0.set_ydata(xti_0)
    
    xn_list = xn_actualiza(fs,fs_veces,xki,dutyc,pulso_ancho,pulso_causal)
    [t_pulsos,x_pulsos,t_unpulso,unpulso] = xn_list # x[n] pulsos, pulso
    linea_x_pulsos.set_xdata(t_pulsos) # x[n] pulsos
    linea_x_pulsos.set_ydata(x_pulsos)
    linea_pulso.set_xdata(t_unpulso) # pulso lti
    linea_pulso.set_ydata(unpulso)

    texto = titulo+' ; f0='+str(f0)
    texto = texto +' ; fs='+str(fs)
    graf_t.set_title(texto)
    
    graf_t.set_xlim([t_pulsos[0],ti[-1]])
    graf_n.set_xlim([t_pulsos[0],ti[-1]])

    fig.canvas.draw_idle() # actualiza figura

# boton reinicio de gráfica
btn_rstdonde = plt.axes([0.85, 0.025, 0.1, 0.04])
btn_rst = Button(btn_rstdonde, 'Reset',
                 hovercolor='0.975')
def grafico_reinicia(event):
    fs_slider.reset()
    return()

# objetos interactivos
fs_slider.on_changed(grafico_actualiza)
btn_rst.on_clicked(grafico_reinicia)

plt.show()

[ interpola pulso ] [ pulso rectangular ] [ algoritmo ] [ gráfica interactiva ]