Skip to content

Commit 3d1a2cd

Browse files
committed
autotune: compensate for airspeed to produce a model at trim airspeed
1 parent 0b08300 commit 3d1a2cd

3 files changed

Lines changed: 65 additions & 22 deletions

File tree

autotune/autotune.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class Window(QDialog):
5757
def __init__(self, parent=None):
5858
super(Window, self).__init__(parent)
5959

60-
self._plot_ref = None
60+
self.model_ref = None
61+
self.input_ref = None
6162
self.closed_loop_ref = None
6263
self.closed_loop_ax = None
6364
self.bode_plot_ref = None
@@ -124,6 +125,16 @@ def __init__(self, parent=None):
124125
layout_tf = self.createTfLayout()
125126
left_menu.addLayout(layout_tf)
126127

128+
129+
trim_group = QFormLayout()
130+
self.line_edit_trim = QDoubleSpinBox()
131+
self.trim_airspeed = 20.0
132+
self.line_edit_trim.setValue(self.trim_airspeed)
133+
self.line_edit_trim.setRange(0.0, 100.0)
134+
self.line_edit_trim.textChanged.connect(self.onTrimChanged)
135+
trim_group.addRow(QLabel("Trim airspeed"), self.line_edit_trim)
136+
left_menu.addLayout(trim_group)
137+
127138
offset_group = QFormLayout()
128139
self.line_edit_offset = QDoubleSpinBox()
129140
self.line_edit_offset.setValue(0.0)
@@ -151,7 +162,8 @@ def __init__(self, parent=None):
151162
self.setLayout(layout_v)
152163

153164
def reset(self):
154-
self._plot_ref = None
165+
self.model_ref = None
166+
self.input_ref = None
155167
self.closed_loop_ref = None
156168
self.bode_plot_ref = None
157169
self.state_plot_refs= []
@@ -206,6 +218,16 @@ def updateCoeffTable(self):
206218
def onModelChanged(self):
207219
self.btn_update_model.setEnabled(True)
208220

221+
def onTrimChanged(self):
222+
try:
223+
self.trim_airspeed = float(self.line_edit_trim.text())
224+
except ValueError:
225+
self.trim_airspeed = 0
226+
self.line_edit_trim.setValue(self.trim_airspeed)
227+
228+
self.btn_run_sys_id.setEnabled(True)
229+
self.plotInputOutput()
230+
209231
def onOffsetChanged(self):
210232
self.plotInputOutput()
211233

@@ -543,17 +565,13 @@ def updateClosedLoop(self):
543565
kd = self.kd
544566
kff = self.kff
545567

546-
airspeed = 40.0
547-
airspeed_trim = 38.0
548-
airspeed_scale = airspeed_trim / airspeed
549568
delays = ctrl.TransferFunction([1], np.append([1], np.zeros(self.sys_id_delays)), dt, inputs='r', outputs='rd')
550569
plant = ctrl.TransferFunction(num, den, dt, inputs='u', outputs='plant_out')
551570
sampler = ctrl.TransferFunction([1], [1, 0], dt, inputs='plant_out', outputs='y')
552571
sum_feedback = ctrl.summing_junction(inputs=['rd', '-y'], output='e')
553572

554573
# Default is standard PID
555574
feedforward = ctrl.TransferFunction([kff], [1], dt, inputs='rd', outputs='ff_out')
556-
ff_scale = ctrl.TransferFunction([airspeed_scale], [1], inputs='ff_out', outputs='ff_out_scaled')
557575
i_control = ctrl.TransferFunction([ki * dt, ki * dt], [2, -2], dt, inputs='e', outputs='i_out') # Integrator discretized using bilinear transform: s = 2(z-1)/(dt(z+1))
558576

559577
# Derivative with 1st order LPF (discretized using Euler method: s = (z-1)/dt)
@@ -565,8 +583,7 @@ def updateClosedLoop(self):
565583

566584
id_control = ctrl.summing_junction(inputs=['e', 'i_out', 'd_out'], output='id_out')
567585
p_control = ctrl.TransferFunction([kc], [1], dt, inputs='id_out', outputs='pid_out')
568-
pid_scale = ctrl.TransferFunction([airspeed_scale**2], [1], inputs='pid_out', outputs='pid_out_scaled')
569-
sum_control = ctrl.summing_junction(inputs=['pid_out_scaled', 'ff_out_scaled'], output='u')
586+
sum_control = ctrl.summing_junction(inputs=['pid_out', 'ff_out'], output='u')
570587

571588
remove_zero = False
572589
no_derivative_kick = True
@@ -579,7 +596,7 @@ def updateClosedLoop(self):
579596
# Derivative on feedback only to remove the "derivative kick"
580597
d_control = ctrl.TransferFunction(-derivative_num, derivative_den, dt, inputs='y', outputs='d_out')
581598

582-
closed_loop = ctrl.interconnect([delays, sampler, sum_feedback, feedforward, sum_control, p_control, i_control, d_control, id_control, pid_scale, ff_scale, plant], inputs='r', outputs='y')
599+
closed_loop = ctrl.interconnect([delays, sampler, sum_feedback, feedforward, sum_control, p_control, i_control, d_control, id_control, plant], inputs='r', outputs='y')
583600

584601
t_out,y_out = ctrl.step_response(closed_loop, T=np.arange(0,1,dt))
585602
self.plotClosedLoop(t_out, y_out)
@@ -624,28 +641,35 @@ def plotBode(self, w, mag, w_cl, mag_cl):
624641
self.canvas.draw()
625642

626643
def plotInputOutput(self, redraw=False):
627-
if self._plot_ref is None or redraw:
644+
if len(self.true_airspeed) == len(self.input):
645+
scale = np.array(self.true_airspeed) / self.trim_airspeed
646+
self.u = self.input * scale**2
647+
648+
if self.model_ref is None or redraw:
628649
# First time we have no plot reference, so do a normal plot.
629650
# .plot returns a list of line <reference>s, as we're
630651
# only getting one we can take the first element.
631652
self.figure.clear()
632653
ax = self.figure.add_subplot(3,3,(1,3))
633-
ax.plot(self.t, self.u, self.t, self.y)
654+
input_ref = ax.plot(self.t, self.u)
655+
self.input_ref = input_ref[0]
656+
ax.plot(self.t, self.y)
634657
plot_refs = ax.plot(0, 0)
635-
self._plot_ref = plot_refs[0]
658+
self.model_ref = plot_refs[0]
636659
ax.set_title("Logged data")
637660
ax.set_xlabel("Time (s)")
638661
ax.set_ylabel("Amplitude")
639662
ax.legend(["Input", "Output", "Model"])
640663
else:
641664
# We have a reference, we can use it to update the data for that line.
642-
self._plot_ref.set_xdata(self.t_est)
665+
self.model_ref.set_xdata(self.t_est)
643666
try:
644667
offset = float(self.line_edit_offset.text())
645668
except ValueError:
646669
offset = 0
647670

648-
self._plot_ref.set_ydata(self.y_est + offset)
671+
self.model_ref.set_ydata(self.y_est + offset)
672+
self.input_ref.set_ydata(self.u)
649673

650674
self.canvas.draw()
651675

@@ -660,8 +684,10 @@ def loadLog(self):
660684
if select.exec_():
661685
self.reset()
662686
self.t = select.t - select.t[0]
663-
self.u = select.u
687+
self.input = select.u
688+
self.u = self.input
664689
self.y = select.y
690+
self.true_airspeed = select.v
665691
self.refreshInputOutputData()
666692
self.runIdentification()
667693
self.computeController()
@@ -678,6 +704,8 @@ def resampleData(self, dt):
678704
self.t = np.arange(0, self.t[-1]+self.dt, self.dt)
679705
self.u = resample(self.u, len(self.t))
680706
self.y = resample(self.y, len(self.t))
707+
self.true_airspeed = resample(self.true_airspeed, len(self.t))
708+
self.input = resample(self.input, len(self.t))
681709

682710
class DoubleSlider(QSlider):
683711

autotune/data_extractor.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,18 @@ def getInputOutputData(logfile, axis, t_start=0.0, t_stop=0.0, instance=0):
5050
u_data = get_data(log, 'vehicle_torque_setpoint', 'xyz[{}]'.format(axis))
5151
t_u_data = us2s(get_data(log, 'vehicle_torque_setpoint', 'timestamp'))
5252

53+
v_data = get_data(log, 'airspeed_validated', 'true_airspeed_m_s')
54+
t_v_data = us2s(get_data(log, 'airspeed_validated', 'timestamp'))
55+
5356
if not np.any(u_data):
5457
# Check for legacy topics
5558
actuator_controls_n = 'actuator_controls_{}'.format(instance)
5659
u_data = get_data(log, actuator_controls_n, 'control[{}]'.format(axis))
5760
t_u_data = us2s(get_data(log, actuator_controls_n, 'timestamp'))
5861

59-
(t_aligned, u_aligned, y_aligned) = extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t_start, t_stop)
62+
(t_aligned, u_aligned, y_aligned, v_aligned) = extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t_v_data, v_data, t_start, t_stop)
6063

61-
return (t_aligned, u_aligned, y_aligned)
64+
return (t_aligned, u_aligned, y_aligned, v_aligned)
6265

6366
def get_data(log, topic_name, variable_name, instance=0):
6467
variable_data = np.array([])
@@ -82,16 +85,19 @@ def get_delta_mean(data_list):
8285
dx = dx/(length-1)
8386
return dx
8487

85-
def extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t_start, t_stop):
88+
def extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t_v_data, v_data, t_start, t_stop):
8689
status_data = get_data(log, 'autotune_attitude_control_status', 'state')
8790
t_status = us2s(get_data(log, 'autotune_attitude_control_status', 'timestamp'))
8891

8992
len_y = len(t_y_data)
9093
len_s = len(t_status)
94+
len_v = len(t_v_data)
9195
i_y = 0
9296
i_s = 0
97+
i_v = 0
9398
u_aligned = []
9499
y_aligned = []
100+
v_aligned = []
95101
t_aligned = []
96102
axis_to_state = [2, 4, 6] # roll, pitch, yaw states
97103

@@ -106,6 +112,9 @@ def extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t
106112
while t_y_data[i_y] <= t_u and i_y < len_y-1:
107113
i_y += 1
108114

115+
while i_v < len_v-1 and t_v_data[i_v] <= t_u:
116+
i_v += 1
117+
109118
if len_s > 0:
110119
while t_status[i_s] <= t_u and i_s < len_s-1:
111120
i_s += 1
@@ -117,12 +126,18 @@ def extract_identification_data(log, t_u_data, u_data, t_y_data, y_data, axis, t
117126
y_aligned.append(y_data[i_y-1])
118127
t_aligned.append(t_u)
119128

129+
if i_v > 0:
130+
v_aligned.append(v_data[i_v-1])
131+
120132
elif t_u >= t_start and t_u <= t_stop:
121133
u_aligned.append(u_data[i_u])
122134
y_aligned.append(y_data[i_y-1])
123135
t_aligned.append(t_u)
124136

125-
return (t_aligned, u_aligned, y_aligned)
137+
if i_v > 0:
138+
v_aligned.append(v_data[i_v-1])
139+
140+
return (t_aligned, u_aligned, y_aligned, v_aligned)
126141

127142
def printCppArrays(t_aligned, u_aligned, y_aligned):
128143
# Print data in c++ arrays
@@ -166,5 +181,5 @@ def printCppArrays(t_aligned, u_aligned, y_aligned):
166181
logfile = os.path.abspath(args.logfile) # Convert to absolute path
167182
axis = {'x':0, 'y':1, 'z':2}[args.axis]
168183

169-
(t_aligned, u_aligned, y_aligned) = getInputOutputData(logfile, axis, instance=0)
184+
(t_aligned, u_aligned, y_aligned, v_aligned) = getInputOutputData(logfile, axis, instance=0)
170185
printCppArrays(t_aligned, u_aligned, y_aligned)

autotune/data_selection_window.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __init__(self, filename):
4646

4747
def loadLog(self):
4848
if self.t_stop > self.t_start:
49-
(self.t, self.u, self.y) = getInputOutputData(self.file_name, self.axis, self.t_start, self.t_stop)
49+
(self.t, self.u, self.y, self.v) = getInputOutputData(self.file_name, self.axis, self.t_start, self.t_stop)
5050
self.accept()
5151
else:
5252
self.printRangeError()
@@ -73,7 +73,7 @@ def loadZData(self):
7373
def refreshInputOutputData(self, axis=0):
7474
if self.file_name:
7575
self.axis = axis
76-
(self.t, self.u, self.y) = getInputOutputData(self.file_name, axis)
76+
(self.t, self.u, self.y, self.v) = getInputOutputData(self.file_name, axis)
7777
self.plotInputOutput(redraw=True)
7878

7979
def plotInputOutput(self, redraw=False):

0 commit comments

Comments
 (0)