Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 168 additions & 20 deletions autotune/autotune.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@
from system_identification import SystemIdentification


def create2ndOrderLpf(fc, zeta, fs):
T = 1.0 / fs
wn = 2.0 * np.pi * fc
K = wn / np.tan(wn * T / 2.0)
K2 = K**2
b2a = wn**2
a0a = 1.0
a1a = 2.0 * zeta * wn
a2a = wn**2
D = a0a * K2 + a1a * K + a2a
b0_prime = b2a
b1_prime = 2.0 * b2a
b2_prime = b2a
a0_prime = D
a1_prime = 2.0 * a2a - 2.0 * K2
a2_prime = a0a * K2 - a1a * K + a2a

b = [b0_prime / D, b1_prime / D, b2_prime / D]
a = [a0_prime / D, a1_prime / D, a2_prime / D]
return b, a


def isNumber(value):
try:
float(value)
Expand All @@ -93,7 +115,7 @@ def __init__(self, parent=None):
self.input_ref = None
self.closed_loop_ref = None
self.closed_loop_ax = None
self.bode_plot_ref = None
self.bode_plot_ref = []
self.pz_plot_refs = []
self.file_name = None
self.is_system_identified = False
Expand All @@ -110,6 +132,8 @@ def __init__(self, parent=None):
self.sys_id_delays = 1
self.sys_id_n_zeros = 2
self.sys_id_n_poles = 2
self.cutoff_freq = 0.0
self.damping_ratio = 0.0

# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
Expand Down Expand Up @@ -198,6 +222,10 @@ def __init__(self, parent=None):
self.tab_gmvc.setLayout(self.createGmvcLayout())
self.tuning_tabs.addTab(self.tab_gmvc, "GMVC")

self.tab_mode = QWidget()
self.tab_mode.setLayout(self.createModeLayout())
self.tuning_tabs.addTab(self.tab_mode, "Mode")

layout_plot = QVBoxLayout()
layout_h.addLayout(left_menu)
layout_h.addLayout(layout_plot)
Expand All @@ -213,7 +241,7 @@ def reset(self):
self.model_ref = None
self.input_ref = None
self.closed_loop_ref = None
self.bode_plot_ref = None
self.bode_plot_ref = []
self.pz_plot_refs = []
self.is_system_identified = False

Expand Down Expand Up @@ -480,6 +508,47 @@ def updateLabelDetune(self):
if self.slider_detune.isSliderDown():
self.computeController()

def createModeLayout(self):
layout_mode = QFormLayout()

layout_cutoff_freq = QHBoxLayout()
self.slider_cutoff_freq = DoubleSlider(Qt.Horizontal)
self.slider_cutoff_freq.setMinimum(0.0)
self.slider_cutoff_freq.setMaximum(100.0)
self.slider_cutoff_freq.setInterval(0.1)
self.slider_cutoff_freq.setValue(0.0)
self.lbl_cutoff_freq = QLabel("{:.1f}".format(self.cutoff_freq))
layout_cutoff_freq.addWidget(self.slider_cutoff_freq)
layout_cutoff_freq.addWidget(self.lbl_cutoff_freq)
self.slider_cutoff_freq.valueChanged.connect(self.updateLabelCutoffFreq)
layout_mode.addRow(QLabel("Cutoff frequency"), layout_cutoff_freq)

layout_damping_ratio = QHBoxLayout()
self.slider_damping_ratio = DoubleSlider(Qt.Horizontal)
self.slider_damping_ratio.setMinimum(0.0)
self.slider_damping_ratio.setMaximum(0.1)
self.slider_damping_ratio.setInterval(0.001)
self.slider_damping_ratio.setValue(0.0)
self.lbl_damping_ratio = QLabel("{:.3f}".format(self.damping_ratio))
layout_damping_ratio.addWidget(self.slider_damping_ratio)
layout_damping_ratio.addWidget(self.lbl_damping_ratio)
self.slider_damping_ratio.valueChanged.connect(self.updateLabelDampingRatio)
layout_mode.addRow(QLabel("Damping ratio"), layout_damping_ratio)

return layout_mode

def updateLabelCutoffFreq(self):
self.cutoff_freq = self.slider_cutoff_freq.value()
self.lbl_cutoff_freq.setText("{:.1f}".format(self.cutoff_freq))
if self.slider_cutoff_freq.isSliderDown():
self.updateClosedLoop()

def updateLabelDampingRatio(self):
self.damping_ratio = self.slider_damping_ratio.value()
self.lbl_damping_ratio.setText("{:.3f}".format(self.damping_ratio))
if self.slider_damping_ratio.isSliderDown():
self.updateClosedLoop()

def runIdentification(self):
n_steps = len(self.t)

Expand Down Expand Up @@ -538,7 +607,7 @@ def plotPolesZeros(self):
poles = self.Gz.poles()
zeros = self.Gz.zeros()
if not self.pz_plot_refs:
ax = self.figure.add_subplot(3, 3, 6)
ax = self.figure.add_subplot(3, 3, 4)
plot_ref = ax.plot(poles.real, poles.imag, "rx", markersize=10)
self.pz_plot_refs.append(plot_ref[0])
plot_ref = ax.plot(zeros.real, zeros.imag, "ro", markersize=10)
Expand Down Expand Up @@ -635,9 +704,18 @@ def updateClosedLoop(self):
outputs="rd",
)
plant = ctrl.TransferFunction(num, den, dt, inputs="u", outputs="plant_out")
sampler = ctrl.TransferFunction(
[1], [1, 0], dt, inputs="plant_out", outputs="y"
)

if self.cutoff_freq > 0.0:
b, a = create2ndOrderLpf(
fc=self.cutoff_freq, zeta=self.damping_ratio, fs=1 / dt
)
else:
b = 1.0
a = 1.0

mode = ctrl.TransferFunction(b, a, dt, inputs="plant_out", outputs="mode_out")

sampler = ctrl.TransferFunction([1], [1, 0], dt, inputs="mode_out", outputs="y")
sum_feedback = ctrl.summing_junction(inputs=["rd", "-y"], output="e")

# Default is standard PID
Expand Down Expand Up @@ -704,6 +782,7 @@ def updateClosedLoop(self):
id_control,
out_sign,
plant,
mode,
],
inputs="r",
outputs="y",
Expand All @@ -727,6 +806,7 @@ def updateClosedLoop(self):
id_control,
out_sign,
plant,
mode,
],
inputs="disturbance",
outputs="y",
Expand All @@ -737,11 +817,28 @@ def updateClosedLoop(self):
y_out += y_d

self.plotClosedLoop(t_out, y_out)
w = np.logspace(-1, 3, 40).tolist()
(mag_cl, phase_cl, omega_cl) = ctrl.frequency_response(
closed_loop, omega=np.asarray(w)

sum_feedback = ctrl.summing_junction(inputs=["rd"], output="e")
open_loop = ctrl.interconnect(
[
delays,
sampler,
sum_feedback,
feedforward,
sum_control,
p_control,
i_control,
d_control,
id_control,
out_sign,
plant,
mode,
],
inputs="r",
outputs="y",
)
self.plotBode(omega_cl, mag_cl)

self.plotBode(open_loop, closed_loop)

def plotClosedLoop(self, t, y):
if self.closed_loop_ref is None:
Expand All @@ -760,21 +857,72 @@ def plotClosedLoop(self, t, y):

self.canvas.draw()

def plotBode(self, w_cl, mag_cl):
if self.bode_plot_ref is None:
ax = self.figure.add_subplot(3, 3, (8, 9))
f = w_cl / (2 * np.pi)
plot_ref = ax.semilogx(f, 20 * np.log10(mag_cl))
def plotBode(self, open_loop, closed_loop):

(
gain_margin,
phase_margin,
stab_margin,
phase_crossover,
gain_crossover,
stab_margin_w,
) = ctrl.stability_margins(open_loop)
stability_margins_text = f"Gain margin: {20 * np.log10(gain_margin):.2f}dB (@{phase_crossover / (2 * np.pi):.1f}Hz)\nPhase margin: {phase_margin:.1f}deg (@{gain_crossover / (2 * np.pi):.1f}Hz)"

w = np.logspace(-1, 3, 500).tolist()
(mag_ol, phase_ol, omega_ol) = ctrl.frequency_response(
open_loop, omega=np.asarray(w)
)

(mag_cl, phase_cl, omega_cl) = ctrl.frequency_response(
closed_loop, omega=np.asarray(w)
)
f = omega_cl / (2 * np.pi)

if not self.bode_plot_ref:
ax = self.figure.add_subplot(3, 3, (5, 6))
plot_ref = ax.semilogx(f, 20 * np.log10(mag_ol), label="Open-loop")
self.bode_plot_ref.append(plot_ref[0])
plot_ref = ax.semilogx(f, 20 * np.log10(mag_cl), label="Closed-loop")
self.bode_plot_ref.append(plot_ref[0])
ax.set_ylim(-20, 20)
ax.plot([f[0], f[-1]], [0, 0], "k--")
ax.plot([f[0], f[-1]], [-3, -3], "g--")
self.bode_plot_ref = plot_ref[0]

ax.set_title("Bode")
ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("Magnitude (dB)")
ax.legend()

ax = self.figure.add_subplot(3, 3, (8, 9))
plot_ref = ax.semilogx(f, phase_ol * 180 / np.pi, label="Open-loop")
self.bode_plot_ref.append(plot_ref[0])
plot_ref = ax.semilogx(f, phase_cl * 180 / np.pi, label="Closed-loop")
self.bode_plot_ref.append(plot_ref[0])
ax.set_ylim(-180, 180)

ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("Phase (deg)")
self.gain_margin_text_ref = ax.text(
0.01,
0.9,
stability_margins_text,
verticalalignment="top",
transform=ax.transAxes,
)

else:
f = w_cl / (2 * np.pi)
self.bode_plot_ref.set_xdata(f)
self.bode_plot_ref.set_ydata(20 * np.log10(mag_cl))
self.bode_plot_ref[0].set_xdata(f)
mag_ol_db = 20 * np.log10(mag_ol)
self.bode_plot_ref[0].set_ydata(mag_ol_db)
self.bode_plot_ref[1].set_xdata(f)
mag_cl_db = 20 * np.log10(mag_cl)
self.bode_plot_ref[1].set_ydata(mag_cl_db)
self.gain_margin_text_ref.set_text(stability_margins_text)

self.bode_plot_ref[2].set_xdata(f)
self.bode_plot_ref[2].set_ydata(phase_ol * 180 / np.pi)
self.bode_plot_ref[3].set_xdata(f)
self.bode_plot_ref[3].set_ydata(phase_cl * 180 / np.pi)

self.canvas.draw()

Expand Down
Loading