G(s) = (3s+1)/(s^2+4s+1)
Es frecuente recurrir a librerías especializadas en control de procesos, como puede ser Control System Toolbox de Matlab o ControlSystems.jl para representar los diagramas de Bode.
Vamos a ver que podemos representar estos diagramas utilizando Julia sin ninguna librería especializada. Esta entrada es la explicación larga de este toot:
Un ejemplo simple
Como ejemplo representaremos el diagrama de Bode de esta función de transferencia:
\[G(s) = \frac{3s+1}{s^2+4s+1}\]
Empezamos definiendo la función de transferencia del ejemplo:
A continuación hacemos el cambio de variable \(s=\mathrm{i}\omega\) y encontramos la parte real y la parte imaginaria:
\[\begin{align} x(\omega) &= Re(G(\mathrm{i}\omega))\\ y(\omega) &= Im(G(\mathrm{i} \omega)) \end{align}\]
donde \(\mathrm{i} = \sqrt{-1}\) y \(\omega\) es la frecuencia angular.
En Julia, simplemente tenemos que reproducir estas funciones:
x(ω) = real(G(im*ω))
y(ω) = imag(G(im*ω))
La razón de amplitudes es:
\[RA(\omega) = \sqrt{x^2(\omega)+y^2(\omega)}\]
y el desfase es:
\[\phi(\omega) = \arctan\frac{y(\omega)}{x(\omega)}\].
RA(ω) = sqrt(x(ω)^2 + y(ω)^2)
φ(ω) = atan(y(ω)/x(ω))*180/pi
Representaremos el diagrama de Bode para valores de \(\omega\) entre 10-2 y 102, calcularemos 100 puntos:
= 10 .^LinRange(-2, 2, 100) ω
100-element Vector{Float64}:
0.010000000000000002
0.010974987654930556
0.012045035402587823
0.013219411484660288
0.014508287784959401
0.015922827933410922
0.017475284000076828
0.019179102616724886
0.0210490414451202
0.023101297000831605
0.025353644939701114
0.02782559402207126
0.030538555088334154
⋮
35.93813663804626
39.442060594376564
43.28761281083057
47.50810162102798
52.14008287999685
57.2236765935022
62.80291441834253
68.92612104349695
75.64633275546291
83.02175681319744
91.11627561154896
100.0
Ya solo queda representar la razón de amplitudes frente a la frecuencia angular. Normalmente utilizo la librería Plots.jl para representar gráficos, pero quiero probar Makie.jl:
using CairoMakie
= Figure()
fig_RA = Axis(fig_RA[1,1],
ax_RA = "ω", xscale = log10,
xlabel = "RA", yscale = log10,
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(10))
yminorticks lines!(ax_RA, ω, RA.(ω))
fig_RA
A continuación representaremos el desfase:
= Figure()
fig_φ = Axis(fig_φ[1,1],
ax_φ = "ω", xscale = log10,
xlabel = "φ",
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(4))
yminorticks lines!(ax_φ, ω, φ.(ω))
fig_φ
Normalmente se dibujan las dos figuras juntas:
= Figure()
fig = Axis(fig[1,1],
ax_RA = "ω", xscale = log10,
xlabel = "RA", yscale = log10,
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(10))
yminorticks = Axis(fig[2,1],
ax_φ = "ω", xscale = log10,
xlabel = "φ",
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(4))
yminorticks lines!(ax_RA, ω, RA.(ω))
lines!(ax_φ, ω, φ.(ω))
fig
Solución a los dolores de cabeza del arco tangente
Veamos un caso más complejo:
G(s) = 20*(s+1)/(s*(s+5)*(s^2+2s+10))
x(ω) = real(G(im*ω))
y(ω) = imag(G(im*ω))
RA(ω) = sqrt(x(ω)^2 + y(ω)^2)
φ(ω) = atan(y(ω)/x(ω))*180/pi
= 10 .^LinRange(-2, 2, 1000)
ω
= Figure()
fig = Axis(fig[1,1],
ax_RA = "ω", xscale = log10,
xlabel = "RA", yscale = log10,
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(10))
yminorticks = Axis(fig[2,1],
ax_φ = "ω", xscale = log10,
xlabel = "φ",
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(4))
yminorticks lines!(ax_RA, ω, RA.(ω))
lines!(ax_φ, ω, φ.(ω))
fig
Tenemos un problema y es que para un valor de \(\omega\) de un poco más de 2 el desfase ha dado una vuelta, lo que provoca esa discontinuidad. Lo más sencillo para solucionar este problema es utilizar la función unwrap!()
de la librería DSP.jl.
using DSP:unwrap!
= unwrap!([φ(i) for i in ω])
φᵤ
= Figure()
fig_φ = Axis(fig_φ[1,1],
ax_φ = "ω", xscale = log10,
xlabel = "φ",
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(4))
yminorticks lines!(ax_φ, ω, φᵤ)
fig_φ
Es importante destacar que hemos aumentado en número de puntos para los que hemos calculado el desfase de 100 a 1000 para que el algoritmo detecte correctamente las vueltas.
Una función para no escribir tanto
Podemos crear una función que reuna todo el código anterior:
function bode(G; w_min=1e-2, w_max=1e2, puntos=100)
x(ω) = real(G(im*ω))
y(ω) = imag(G(im*ω))
RA(ω) = sqrt(x(ω)^2 + y(ω)^2)
φ(ω) = atan(y(ω)/x(ω))*180/pi
= 10 .^LinRange(log10(w_min), log10(w_max), puntos)
ω
= unwrap!([φ(i) for i in ω])
φᵤ
= Figure()
fig = Axis(fig[1,1],
ax_RA = "ω", xscale = log10,
xlabel = "RA", yscale = log10,
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(10))
yminorticks = Axis(fig[2,1],
ax_φ = "ω", xscale = log10,
xlabel = "φ",
ylabel =true, xminorticksvisible=true,
xminorgridvisible= IntervalsBetween(10),
xminorticks =true, yminorticksvisible=true,
yminorgridvisible= IntervalsBetween(4))
yminorticks lines!(ax_RA, ω, RA.(ω))
lines!(ax_φ, ω, φᵤ)
figend
bode (generic function with 1 method)
Comprobemos que funciona:
G(s) = 3/(2s^2+s+1)*exp(-.5s)
bode(G; w_min=0.1, w_max=10, puntos=500)
Conclusión
Como vemos crear una función que dibuje los diagramas de Bode (o de Nyquist, si nos apetece) no es complicado. Julia nos permite aplicar la teoría de manera prácticamente directa, ¡lo que es fantástico!
A partir de hay podemos crear nuevas funciones para crear nuestra propia caja de herramientas. ¿Ventajas? Conocemos exactamente lo que hace el código, además de aprender. ¿Inconvenientes? Es fácil caer en soluciones naïf que no tengan en cuenta algunos casos o que el código sea un tanto “chapucero”.