Ecuaciones en diferencias

Código auxiliar

# Cargo paquetitos
using DSP, FFTW, Statistics, WAV
function wavread_mono(file)
  x, sr = wavread(file)
  return mean(x; dims=2)[:], sr
end
# Y armo un par de funciones auxiliares
stem(args...; kwargs...) = sticks(args...; 
  																marker=:circle, 
  																leg=false, 
  																kwargs...)
zeropolegain(pr) = DSP.ZeroPoleGain(pr)
zeropolegain(z, p, g) = DSP.ZeroPoleGain(z, p, g)
polynomialratio(zpg) = DSP.PolynomialRatio(zpg)
function polynomialratio(b, a)
  n = max(length(a), length(b))
  return DSP.PolynomialRatio(padright(b, n), padright(a, n))
end
getpoles(zpg) = DSP.ZeroPoleGain(zpg).p
getzeros(zpg) = DSP.ZeroPoleGain(zpg).z
getgain(zpg) = DSP.ZeroPoleGain(zpg).k
getnumcoefs(pr) = trimlastzeros!(reverse(DSP.PolynomialRatio(pr).b.coeffs))
getdencoefs(pr) = trimlastzeros!(reverse(DSP.PolynomialRatio(pr).a.coeffs))
function trimlastzeros!(a)
  !iszero(a[end]) && return a
  pop!(a)
  return trimlastzeros!(a)
end
DSP.filt(zpg::DSP.ZeroPoleGain, r...; kwargs...) = filt(polynomialratio(zpg), r...; kwargs...)
function zplane(zs, ps; kwargs...)
	scatter(real.(zs), imag.(zs);
		  marker = (:black, :circle), label="Cero", kwargs...)
	scatter!( real.(ps), imag.(ps);
	  	marker = (:red, :xcross), label="Polo", kwargs...)  
  ts = range(0,stop=2pi;length=100)
  plot!(cos.(ts), sin.(ts); aspect_ratio = 1, kwargs...)
end
function zplane(zpg; kwargs...)
	zs = getzeros(zpg)
	ps = getpoles(zpg)
	isempty(zs) && isempty(ps) && (append!(ps, 0))
	
	return zplane(zs, ps; kwargs...)
end
zplane(pr::DSP.PolynomialRatio; kwargs...) = zplane(DSP.ZeroPoleGain(pr); kwargs...)
# Delta
d(n) = n == 0 ? 1. : 0. 
# Escalón
u(n) = n >= 0 ? 1. : 0. 
using Plots
Plots.default(:legend, false)
# Pad vector with zeros on the right until its length is `n`
padright(x, n) = copyto!(zeros(eltype(x), n), x)
"""
Función módulo pero con offset (opcional)
Manda a `t` al intervalo [from, from+length)
sumándole o restándole algún múltiplo de `len`
"""
cshift(t, len, from=0) = mod(t - from, len) + from
# Espectrograma
using IterTools
function stft(x; overlap, window, nfft, rest...)
  nwin = length(window)
  @assert overlap < nwin
  res = [ fft(padright(xseg .* window, nfft)) 
    for xseg in partition(x, nwin, nwin - overlap)]
  
  return [ res[i][j] for j in 1:nfft, i in eachindex(res)]
end
function specgram(x; 
    overlap=0.5, 
    window=hamming(div(length(x), 16)), 
    nfft=length(window), 
    rest...)
  
  window isa Integer && (window = rect(window))
  overlap isa AbstractFloat && (overlap = round(Int, length(window) * overlap))
    
  return stft(x; overlap=overlap, window=window, nfft=nfft)
end
  
specplot(x::AbstractMatrix; kwargs...) = @error "You are entering a Matrix (2D Array). I need a Vector (1D Array)."
function specplot(x::AbstractVector; 
      fs=1, 
      onesided=false, 
      xaxis="Tiempo (s)", 
      yaxis="Frecuencia (Hz)",
      kws...)
    mat = specgram(x; kws...)
  
    fmax = fs
    if onesided
      mat = mat[1:div(size(mat, 1) + 2, 2), :]
      fmax = fs/2
    end
  
  times = range(0; length=size(mat, 2), stop=length(x)/fs) # aprox
  freqs = range(0; length=size(mat, 1), stop=fmax)
  
	# Reubico las frecuencias negativas arriba de todo
  if !onesided
    freqs = cshift.(freqs, fs, -fs/2)
    ord   = sortperm(freqs)
    mat   = mat[ord, :]
    freqs = freqs[ord]
  end
	return heatmap(times, freqs, log.(abs.(mat) .+ eps()); 
          xaxis=xaxis, yaxis=yaxis,
          seriescolor=:bluesreds, legend=true, kws...)
 return times, freqs, mat 
end
function specplot(x :: AbstractVector{<:AbstractFloat}; kws...)
    return specplot(convert.(Complex, x); onesided=true, kws...)
end
38.0s
Julia
specplot (generic function with 3 methods)

Algunas funciones para trabajar con sistemas definidos por ecuaciones en diferencias

Los sistemas LTI definidos por ecuaciones en diferencias tiene una transferencia H(z) racional. Quedan totalmente definidos por los coeficientes del polinomino del numerador y del denominador. Alternativamente, por la lista de polos, ceros, y un valor de ganancia.

Los sistemas FIR (respuesta al impulso finita) expresan la salida como una convolución que ya es una ecuación en diferencias, de orden 0 (sólo aparece y(n)). El denominador de H(z) es 1. No tiene polos (salvo quizás en 0 o infinito).

Si queremos ver la salida de un sistema FIR, usamos la función `conv`.

Los sistemas IIR no se pueden implementar con la función `conv` ya que h(n) es infinita. Pero, los que vienen de ecuaciones en diferencias, sí. La PC implementa la versión inicialmente en reposo, iterando hasta cierto n.

La función que hace esto se llama `filt` (en MATLAB y otros, `filter`).

#=
Si se quiere tener la salida inicialmente en reposo para este sistema
a[1] * y(n) + a[2] * y(n-1) +... = b[1] * x(n) + b[2] * x(n-1) + ...
frente a una entrada definida pro el vector x (x(n) = 0 para n<0, x(0)=x[1], etc)
uno usa
filt(b, a, x)
La salida tendrá tantos elementos como x
=#
# Caso FIR
filt([0, 1], [1], 1:10)
8.7s
Julia
10-element Array{Int64,1}: 0 1 2 3 4 5 6 7 8 9
# Caso IIR
a = [1, 3.4] # Sin pérdida de generalidad, a(1) puede ser siempre 1
b = [3.2, 1.3, 6.7]
filt(b, a, 1:3)
5.6s
Julia
3-element Array{Float64,1}: 3.2 -3.18 29.712
# Si querés más valores, meté una entrada más larga. Si terminó la entrada, agregar ceros
filt(b, a, padright(1:3, 5))
4.2s
Julia
5-element Array{Float64,1}: 3.2 -3.18 29.712 -83.7208 304.751

Se puede hacer un diagramita de polos y ceros a partir del vector de polos y de ceros con la función zplane

zs = [-1.34, 0]
ps = 0.8 .* exp.(im .* range(0; step=2pi/10, length=10))
zplane(zs, ps)
39.2s
Julia
# Podemos armar un objeto que representa un filtro IIR a partir de los polos, ceros y ganancia
gain = 1
zpg = zeropolegain(zs, ps, gain);
0.2s
Julia
# y recuperar los zeros
getzeros(zpg)
0.2s
Julia
2-element Array{Float64,1}: -1.34 0.0
# o polos
getpoles(zpg)
1.3s
Julia
10-element Array{Complex{Float64},1}: 0.8+0.0im 0.647214+0.470228im 0.247214+0.760845im -0.247214+0.760845im -0.647214+0.470228im -0.8+9.79717e-17im -0.647214-0.470228im -0.247214-0.760845im 0.247214-0.760845im 0.647214-0.470228im
# o ganancia
getgain(zpg)
0.2s
Julia
1
# o tb podemos armar un objeto en términos de los vectores de coeficientes a y b definidos como para filt
pr = polynomialratio(b, a);
# y recuperar numerador
getnumcoefs(pr)
0.6s
Julia
3-element Array{Float64,1}: 3.2 1.3 6.7
# o denominador
getdencoefs(pr)
0.3s
Julia
2-element Array{Float64,1}: 1.0 3.4
# Cualquiera de estas se pueden usar con zplane
zplane(zpg)
0.6s
Julia
zplane(pr)
2.0s
Julia
# y cualquiera permite extraer polos, ceros
getzeros(pr), getpoles(pr)
2.4s
Julia
(Complex{Float64}[-0.203125-1.43265im, -0.203125+1.43265im], Complex{Float64}[-3.4+0.0im, 0.0+0.0im])
# o coeficientes del polinomio numerador y denominador
getnumcoefs(zpg), getdencoefs(zpg)
1.1s
Julia
([1.0, 1.34], [1.0, 2.22045e-16, 0.0, 5.55112e-16, 6.10623e-16, 9.4369e-16, -4.71845e-16, -1.31839e-15, 2.28983e-16, 4.16334e-17, -0.107374])
# filt acepta estos objetos también
filt(pr, 1:3), filt(b, a, 1:3), filt(zpg, 1:3)
1.1s
Julia
([3.2, -3.18, 29.712], [3.2, -3.18, 29.712], [1.0, 3.34, 5.68])
# También podés usar estos objetos en la función freqz, que
# devuelve la respuesta en frecuencia del sistema estable
# evaluada en cierto omegón (ojo que el estable puede no ser el
# inicialmente en reposo)
w = pi/4
freqz(pr, w)
1.0s
Julia
1.86204+0.923164im
# No hace más que la cuentita de evaluar H(z=exp(im*w))
cuentita(w) = sum(b .* exp(-im*w) .^ (0 : length(b) - 1)) / 
  sum(a .* exp(-im*w) .^ (0 : length(a) - 1))
cuentita(w)
0.8s
Julia
1.86204-0.923164im
fs = range(0; stop=pi, length=300)
plot(fs, abs.(freqz(pr, fs)))
1.8s
Julia
Runtimes (1)