Skip to content

Commit 31ddedb

Browse files
committed
autotune: add possibility to select any signal for input/output
1 parent ba50c3b commit 31ddedb

2 files changed

Lines changed: 151 additions & 103 deletions

File tree

autotune/data_extractor.py

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,51 @@
4141
from pyulog import ULog
4242
from scipy.interpolate import make_interp_spline
4343

44-
def getInputOutputData(logfile, axis, t_start=0.0, t_stop=0.0, instance=0):
45-
log = ULog(logfile)
44+
class FieldDefinition():
45+
def __init__(self, topic, var, inst):
46+
self.topic_name = topic
47+
self.variable_name = var
48+
self.instance = inst
4649

47-
y_data = get_data(log, 'vehicle_angular_velocity', 'xyz[{}]'.format(axis))
48-
t_y_data = us2s(get_data(log, 'vehicle_angular_velocity', 'timestamp'))
50+
class DataExtractor():
51+
def __init__(self, logfile_name):
52+
self.log = ULog(logfile_name)
4953

50-
u_data = get_data(log, 'vehicle_torque_setpoint', 'xyz[{}]'.format(axis))
51-
t_u_data = us2s(get_data(log, 'vehicle_torque_setpoint', 'timestamp'))
54+
def get_topics_list(self):
55+
fields = []
56+
for elem in self.log.data_list:
57+
for var_name in elem.data.keys():
58+
fields.append(FieldDefinition(elem.name, var_name, elem.multi_id))
5259

53-
v_data = get_data(log, 'airspeed_validated', 'true_airspeed_m_s')
54-
t_v_data = us2s(get_data(log, 'airspeed_validated', 'timestamp'))
60+
return fields
5561

56-
if not np.any(u_data):
57-
# Check for legacy topics
58-
actuator_controls_n = 'actuator_controls_{}'.format(instance)
59-
u_data = get_data(log, actuator_controls_n, 'control[{}]'.format(axis))
60-
t_u_data = us2s(get_data(log, actuator_controls_n, 'timestamp'))
62+
def getPreview(self, field_def):
63+
(t_data, data) = self.getData(field_def)
6164

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)
65+
if(len(t_data) > 10e3):
66+
# Downsample to speed up plotting preview
67+
downsampling_factor = int(len(t_data)/10e3)+1
68+
t_data = t_data[:-downsampling_factor+1:downsampling_factor]
69+
data = data[:-downsampling_factor+1:downsampling_factor]
6370

64-
return (t_aligned, u_aligned, y_aligned, v_aligned)
71+
return (t_data, data)
72+
73+
def getData(self, field_def):
74+
data = get_data(self.log, field_def.topic_name, field_def.variable_name, field_def.instance)
75+
t_data = us2s(get_data(self.log, field_def.topic_name, 'timestamp', field_def.instance))
76+
77+
return (t_data, data)
78+
79+
def getInputOutputData(self, field_def_u, field_def_y, t_start=0.0, t_stop=0.0):
80+
(t_u_data, u_data) = self.getData(field_def_u)
81+
(t_y_data, y_data) = self.getData(field_def_y)
82+
83+
v_data = get_data(self.log, 'airspeed_validated', 'true_airspeed_m_s')
84+
t_v_data = us2s(get_data(self.log, 'airspeed_validated', 'timestamp'))
85+
86+
(t_aligned, u_aligned, y_aligned, v_aligned) = resampleIdentificationData(t_u_data, u_data, t_y_data, y_data, t_v_data, v_data, t_start, t_stop)
87+
88+
return (t_aligned, u_aligned, y_aligned, v_aligned)
6589

6690
def get_data(log, topic_name, variable_name, instance=0):
6791
variable_data = np.array([])
@@ -89,31 +113,34 @@ def resample_interp(t, u, t_new):
89113
interp = make_interp_spline(t, u, k=1)
90114
return interp(t_new)
91115

92-
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):
93-
if t_start == 0.0 and t_stop == 0.0:
94-
# Find autotune sequence
95-
status_data = get_data(log, 'autotune_attitude_control_status', 'state')
96-
t_status = us2s(get_data(log, 'autotune_attitude_control_status', 'timestamp'))
97-
axis_to_state = [2, 4, 6] # roll, pitch, yaw states
116+
def find_autotune_sequence(log, axis):
117+
t_start = None
118+
t_stop = None
119+
status_data = get_data(log, 'autotune_attitude_control_status', 'state')
120+
t_status = us2s(get_data(log, 'autotune_attitude_control_status', 'timestamp'))
121+
axis_to_state = [2, 4, 6] # roll, pitch, yaw states
98122

99-
status_prev = 0
123+
status_prev = 0
100124

101-
for i_s in range(len(t_status)):
102-
if status_data[i_s] == axis_to_state[axis]:
103-
if status_prev != axis_to_state[axis]:
104-
t_start = t_status[i_s]
125+
for i_s in range(len(t_status)):
126+
if status_data[i_s] == axis_to_state[axis]:
127+
if status_prev != axis_to_state[axis]:
128+
t_start = t_status[i_s]
129+
130+
else:
131+
if status_prev == axis_to_state[axis]:
132+
t_stop = t_status[i_s]
133+
break
105134

106-
else:
107-
if status_prev == axis_to_state[axis]:
108-
t_stop = t_status[i_s]
109-
break
135+
status_prev = status_data[i_s]
110136

111-
status_prev = status_data[i_s]
137+
return (t_start, t_stop)
112138

113-
if t_start == 0.0:
139+
def resampleIdentificationData(t_u_data, u_data, t_y_data, y_data, t_v_data, v_data, t_start, t_stop):
140+
if not t_start:
114141
t_start = t_u_data[0]
115142

116-
if t_stop == 0.0:
143+
if not t_stop:
117144
t_stop = t_u_data[-1]
118145

119146
dt = get_delta_mean(t_y_data)

autotune/data_selection_window.py

Lines changed: 91 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFormLayout, QRadioButton, QMessageBox, QFileDialog
1+
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QPushButton, QLabel, QFormLayout, QRadioButton, QMessageBox, QFileDialog, QComboBox
22

33
import matplotlib.pyplot as plt
44
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
55
from matplotlib.widgets import SpanSelector
66

77
import numpy as np
88

9-
from data_extractor import getInputOutputData
9+
from data_extractor import DataExtractor
1010

1111
class DataSelectionWindow(QDialog):
1212
def __init__(self, filename):
1313
QDialog.__init__(self)
1414

15+
self.t = []
16+
self.u = []
17+
self.y = []
18+
self.t_start = None
19+
self.t_stop = None
20+
21+
self.input_ref = None
22+
self.output_ref = None
1523
self.figure = plt.figure(1)
1624
self.canvas = FigureCanvas(self.figure)
25+
self.initPlot()
1726

1827
layout_v = QVBoxLayout()
1928

@@ -22,51 +31,59 @@ def __init__(self, filename):
2231
btn_browse.clicked.connect(self.browseFiles)
2332
top_group.addWidget(btn_browse)
2433

34+
in_out_group = QFormLayout()
35+
self.combo_u = QComboBox()
36+
self.combo_u.setEditable(True)
37+
self.combo_u.setInsertPolicy(QComboBox.NoInsert)
38+
self.combo_u.currentIndexChanged.connect(self.selectUData)
39+
in_out_group.addRow(QLabel("Input:"), self.combo_u)
40+
41+
self.combo_y = QComboBox()
42+
self.combo_y.setEditable(True)
43+
self.combo_y.setInsertPolicy(QComboBox.NoInsert)
44+
self.combo_y.currentIndexChanged.connect(self.selectYData)
45+
in_out_group.addRow(QLabel("Output:"), self.combo_y)
46+
top_group.addLayout(in_out_group)
47+
2548
layout_v.addLayout(top_group)
2649
layout_v.addWidget(self.canvas)
2750

28-
xyz_group = QHBoxLayout()
29-
r_x = QRadioButton("x")
30-
r_x.setChecked(True)
31-
r_y = QRadioButton("y")
32-
r_z = QRadioButton("z")
33-
xyz_group.addWidget(QLabel("Axis"))
34-
xyz_group.addWidget(r_x)
35-
xyz_group.addWidget(r_y)
36-
xyz_group.addWidget(r_z)
37-
r_x.clicked.connect(self.loadXData)
38-
r_y.clicked.connect(self.loadYData)
39-
r_z.clicked.connect(self.loadZData)
40-
41-
layout_v.addLayout(xyz_group)
42-
4351
btn_ok = QPushButton("Load selection")
44-
btn_ok.clicked.connect(self.loadLog)
52+
btn_ok.clicked.connect(self.loadSelection)
4553
layout_v.addWidget(btn_ok)
4654

4755
self.setLayout(layout_v)
4856

4957
if filename:
5058
self.file_name = filename
51-
self.refreshInputOutputData()
59+
self.openFile()
5260

5361
else:
5462
self.browseFiles()
5563

56-
def loadLog(self):
57-
if self.t_stop > self.t_start:
58-
(self.t, self.u, self.y, self.v) = getInputOutputData(self.file_name, self.axis, self.t_start, self.t_stop)
64+
def loadSelection(self):
65+
if (self.t_start is None and self.t_start is None) or (self.t_stop > self.t_start):
66+
(self.t, self.u, self.y, self.v) = self.data_extractor.getInputOutputData(self.topics[self.index_u], self.topics[self.index_y], self.t_start, self.t_stop)
5967
self.accept()
6068
else:
6169
self.printRangeError()
6270

6371
def browseFiles(self):
6472
options = QFileDialog.Options()
6573
options |= QFileDialog.DontUseNativeDialog
66-
self.file_name, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","ULog (*.ulg)", options=options)
74+
file_name, _ = QFileDialog.getOpenFileName(self,"Select ULog file", "","ULog (*.ulg)", options=options)
75+
self.file_name = file_name
76+
self.openFile()
6777

78+
def openFile(self):
6879
if self.file_name:
69-
self.refreshInputOutputData()
80+
self.data_extractor = DataExtractor(self.file_name)
81+
self.topics = self.data_extractor.get_topics_list()
82+
list_names = [f"{topic.topic_name}/{topic.variable_name}.{topic.instance}" for topic in self.topics]
83+
self.combo_u.clear()
84+
self.combo_u.addItems(list_names)
85+
self.combo_y.clear()
86+
self.combo_y.addItems(list_names)
7087

7188
def printRangeError(self):
7289
msg = QMessageBox()
@@ -75,55 +92,59 @@ def printRangeError(self):
7592
msg.setText("Range is invalid")
7693
msg.exec_()
7794

78-
def loadXData(self):
79-
if self.file_name:
80-
self.refreshInputOutputData(0)
81-
82-
def loadYData(self):
83-
if self.file_name:
84-
self.refreshInputOutputData(1)
85-
86-
def loadZData(self):
87-
if self.file_name:
88-
self.refreshInputOutputData(2)
89-
90-
def refreshInputOutputData(self, axis=0):
91-
if self.file_name:
92-
self.axis = axis
93-
(t, u, y, _) = getInputOutputData(self.file_name, axis)
94-
95-
if(len(t) > 10e3):
96-
# Downsample to speed up plotting preview
97-
downsampling_factor = int(len(t)/10e3)+1
98-
self.t = t[:-downsampling_factor+1:downsampling_factor]
99-
self.u = u[:-downsampling_factor+1:downsampling_factor]
100-
self.y = y[:-downsampling_factor+1:downsampling_factor]
101-
102-
else:
103-
self.t = t
104-
self.u = u
105-
self.y = y
106-
107-
self.plotInputOutput(redraw=True)
108-
109-
def plotInputOutput(self, redraw=False):
110-
self.figure.clear()
111-
self.ax = self.figure.add_subplot(1,1,1)
112-
self.ax.plot(self.t, self.u, self.t, self.y)
113-
self.ax.set_title("Click and drag to select data range")
114-
self.ax.set_xlabel("Time (s)")
115-
self.ax.set_ylabel("Amplitude")
116-
self.ax.legend(["Input", "Output"])
117-
118-
self.span = SpanSelector(self.ax, self.onselect, 'horizontal', useblit=False,
119-
props=dict(alpha=0.2, facecolor='green'), interactive=True)
95+
def selectUData(self, index):
96+
self.index_u = index
97+
(self.t, self.u) = self.data_extractor.getPreview(self.topics[index])
98+
self.plotU()
99+
100+
def selectYData(self, index):
101+
self.index_y = index
102+
(self.t, self.y) = self.data_extractor.getPreview(self.topics[index])
103+
self.plotY()
104+
105+
def initPlot(self):
106+
if self.input_ref is None:
107+
self.figure.clear()
108+
self.ax = self.figure.add_subplot(1,1,1)
109+
plot_refs = self.ax.plot([], [])
110+
self.input_ref = plot_refs[0]
111+
112+
plot_refs = self.ax.plot([], [])
113+
self.output_ref = plot_refs[0]
114+
self.ax.autoscale(False)
115+
116+
self.ax.set_title("Click and drag to select data range")
117+
self.ax.set_xlabel("Time (s)")
118+
self.ax.set_ylabel("Amplitude")
119+
self.ax.legend(["Input", "Output"])
120+
121+
self.span = SpanSelector(self.ax, self.onselect, 'horizontal', useblit=False,
122+
props=dict(alpha=0.2, facecolor='green'), interactive=True)
123+
124+
self.canvas.mpl_connect('scroll_event', self.zoom_fun)
125+
self.canvas.draw()
126+
127+
def plotU(self):
128+
self.input_ref.set_xdata(self.t)
129+
self.input_ref.set_ydata(self.u)
130+
self.resetXYLim()
131+
self.canvas.draw()
120132

121-
self.t_start = self.t[0]
122-
self.t_stop = self.t[-1]
133+
def plotY(self):
134+
self.output_ref.set_xdata(self.t)
135+
self.output_ref.set_ydata(self.y)
136+
self.resetXYLim()
137+
self.canvas.draw()
123138

124-
self.canvas.mpl_connect('scroll_event', self.zoom_fun)
139+
def resetXYLim(self):
140+
self.ax.set_xlim([self.t[0], self.t[-1]])
125141

126-
self.canvas.draw()
142+
if len(self.u) > 0 and len(self.y) > 0:
143+
self.ax.set_ylim([min([min(self.u), min(self.y)]), max([max(self.u), max(self.y)])])
144+
elif len(self.u) > 0:
145+
self.ax.set_ylim([min(self.u), max(self.u)])
146+
elif len(self.y) > 0:
147+
self.ax.set_ylim([min(self.y), max(self.y)])
127148

128149
def onselect(self, xmin, xmax):
129150
indmin, indmax = np.searchsorted(self.t, (xmin, xmax))

0 commit comments

Comments
 (0)