Powered by AppSignal & Oban Pro

Bode Plot Example

examples/bode_plot.livemd

Bode Plot Example

Setup

Mix.install([
  {:nx, "~> 0.13"},
  {:nx_lapack, path: System.fetch_env!("HOME") <> "/Documents/forks/nx_lapack"},
  {:nx_control, path: System.fetch_env!("HOME") <> "/Documents/forks/nx_control"},
  {:vega_lite, "~> 0.1"},
  {:kino_vega_lite, "~> 0.1"}
])

alias VegaLite, as: Vl

System Definition

Consider an open-loop transfer function:

[ G(s) = \frac{1}{s(s+1)(s+10)} ]

tf = NxControl.TransferFunction.new([1], [1, 11, 10, 0])

Frequency Response Data

{freqs, mag, phase} = NxControl.FrequencyResp.bode_data(tf)

Magnitude Plot

data = %{
  log_freq: Nx.to_flat_list(freqs) |> Enum.map(&:math.log10/1),
  magnitude: Nx.to_flat_list(mag)
}

Vl.new(width: 600, height: 300, title: "Bode Plot — Magnitude")
|> Vl.data_from_values(data)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "log_freq", type: :quantitative, title: "log₁₀(ω)")
|> Vl.encode_field(:y, "magnitude", type: :quantitative, title: "Magnitude (dB)")

Phase Plot

data2 = %{
  log_freq: Nx.to_flat_list(freqs) |> Enum.map(&:math.log10/1),
  phase: Nx.to_flat_list(phase)
}

Vl.new(width: 600, height: 300, title: "Bode Plot — Phase")
|> Vl.data_from_values(data2)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "log_freq", type: :quantitative, title: "log₁₀(ω)")
|> Vl.encode_field(:y, "phase", type: :quantitative, title: "Phase (deg)")

Stability Margins

{gm_mag, gm_phase, pm_freq, pm_phase} = NxControl.FrequencyResp.margins(tf)
Margin Value
Gain Margin #{if gm_mag == :infinity, do: “∞”, else: Float.round(gm_mag, 4)}
Phase Margin #{if pm_phase == nil, do: “—“, else: Float.round(pm_phase, 2)}° at #{if pm_freq == nil, do: “—“, else: “#{Float.round(pm_freq, 3)} rad/s”}

MATLAB Time-Constant Form Example

Transfer function defined in MATLAB’s (Ts + 1) format:

f0 = [0.82 1];
f1 = [0.337 1];
f2 = [1.64 1];
f3 = [0.07 1];
den1 = conv(f0, f1)
den2 = conv(f2, f3)
sys = tf(den1, den2)
bode(sys)

Equivalent in nx_control:

# Polynomial multiplication (conv)
f0 = [0.82, 1.0]; f1 = [0.337, 1.0]
f2 = [1.64, 1.0]; f3 = [0.07, 1.0]

poly_mul = fn a, b ->
  len = length(a) + length(b) - 1
  for i <- 0..(len - 1) do
    Enum.reduce(0..i, 0.0, fn j, acc ->
      (Enum.at(a, i - j) || 0.0) * (Enum.at(b, j) || 0.0) + acc
    end)
  end
end

num = poly_mul.(f0, f1)  # 0.27634s² + 1.157s + 1.0
den = poly_mul.(f2, f3)  # 0.1148s² + 1.71s + 1.0

tf_m = NxControl.TransferFunction.new(num, den)
{freqs_m, mag_m, phase_m} = NxControl.FrequencyResp.bode_data(tf_m)

Bode Plot

data_m = %{
  log_freq: Nx.to_flat_list(freqs_m) |> Enum.map(&:math.log10/1),
  magnitude: Nx.to_flat_list(mag_m)
}

Vl.new(width: 600, height: 300, title: "MATLAB Transfer Function — Magnitude")
|> Vl.data_from_values(data_m)
|> Vl.mark(:line, color: "green")
|> Vl.encode_field(:x, "log_freq", type: :quantitative, title: "log₁₀(ω)")
|> Vl.encode_field(:y, "magnitude", type: :quantitative, title: "Magnitude (dB)")
data_m2 = %{
  log_freq: Nx.to_flat_list(freqs_m) |> Enum.map(&:math.log10/1),
  phase: Nx.to_flat_list(phase_m)
}

Vl.new(width: 600, height: 300, title: "MATLAB Transfer Function — Phase")
|> Vl.data_from_values(data_m2)
|> Vl.mark(:line, color: "green")
|> Vl.encode_field(:x, "log_freq", type: :quantitative, title: "log₁₀(ω)")
|> Vl.encode_field(:y, "phase", type: :quantitative, title: "Phase (deg)")