Skip to content

Commit 3bdec39

Browse files
authored
Autotune: add coherence plot for data selection (#38)
1 parent c2011e0 commit 3bdec39

1 file changed

Lines changed: 95 additions & 5 deletions

File tree

autotune/data_selection_window.py

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from matplotlib.widgets import SpanSelector
66

77
import numpy as np
8+
from scipy import signal
89

910
from data_extractor import DataExtractor
1011
from searchable_combo_box import SearchableComboBox
@@ -56,9 +57,11 @@ def __init__(self, filename):
5657

5758
self.input_ref = None
5859
self.output_ref = None
59-
self.figure = plt.figure(1)
60+
self.coherence_ref = None
61+
self.coherence_info_text = None
62+
self.figure = plt.figure(figsize=(8, 6), layout="constrained")
6063
self.canvas = FigureCanvas(self.figure)
61-
self.initPlot()
64+
self.initPlots()
6265

6366
layout_v = QVBoxLayout()
6467

@@ -86,6 +89,10 @@ def __init__(self, filename):
8689
layout_v.addLayout(top_group)
8790
layout_v.addWidget(self.canvas)
8891

92+
self.label_warning = QLabel("")
93+
self.label_warning.setStyleSheet("color: red; font-weight: bold;")
94+
layout_v.addWidget(self.label_warning)
95+
8996
btn_ok = QPushButton("Load selection")
9097
btn_ok.clicked.connect(self.loadSelection)
9198
layout_v.addWidget(btn_ok)
@@ -100,7 +107,7 @@ def __init__(self, filename):
100107
self.browseFiles()
101108

102109
def loadSelection(self):
103-
if (self.t_start is None and self.t_start is None) or (self.t_stop > self.t_start):
110+
if (self.t_start is None and self.t_stop is None) or (self.t_stop > self.t_start):
104111
(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)
105112
self.accept()
106113
else:
@@ -183,10 +190,12 @@ def selectYData(self, index):
183190
def getTrimAirspeed(self):
184191
return self.data_extractor.getTrimAirspeed()
185192

186-
def initPlot(self):
193+
def initPlots(self):
187194
if self.input_ref is None:
188195
self.figure.clear()
189-
self.ax = self.figure.add_subplot(1,1,1)
196+
197+
# --- Time series Axes (Top) ---
198+
self.ax = self.figure.add_subplot(2,1,1)
190199
color_in = 'tab:blue'
191200
plot_refs = self.ax.plot([], [], color=color_in)
192201
self.input_ref = plot_refs[0]
@@ -207,6 +216,16 @@ def initPlot(self):
207216

208217
self.span = SpanSelector(self.ax_out, self.onselect, 'horizontal', useblit=False,
209218
props=dict(alpha=0.2, facecolor='green'), interactive=True)
219+
220+
# --- Coherence Plot (Bottom) ---
221+
self.ax_coherence = self.figure.add_subplot(2, 1, 2)
222+
color_coherence = 'tab:grey'
223+
plot_refs = self.ax_coherence.plot([], [], color=color_coherence)
224+
self.coherence_ref = plot_refs[0]
225+
self.ax_coherence.set_title("Coherence")
226+
self.ax_coherence.set_xlabel("Frequency (Hz)")
227+
self.ax_coherence.set_ylabel("Coherence")
228+
self.ax_coherence.set_xscale('log')
210229

211230
self.canvas.mpl_connect('scroll_event', self.zoom_fun)
212231
self.canvas.draw()
@@ -241,6 +260,77 @@ def onselect(self, xmin, xmax):
241260
self.ax.set_xlim(self.t_start - 1.0, self.t_stop + 1.0)
242261
self.canvas.draw()
243262

263+
self.plotCoherence()
264+
265+
def plotCoherence(self):
266+
if len(self.t) == 0 or len(self.u) == 0 or len(self.y) == 0:
267+
return
268+
269+
# Use getInputOutputData with selected range
270+
if self.t_start is not None and self.t_stop is not None and self.t_stop > self.t_start:
271+
t_sel, u_sel, y_sel, _ = self.data_extractor.getInputOutputData(
272+
self.topics[self.index_u], self.topics[self.index_y],
273+
self.t_start, self.t_stop
274+
)
275+
else:
276+
# If no range selected, just use full duration
277+
t_sel, u_sel, y_sel, _ = self.data_extractor.getInputOutputData(
278+
self.topics[self.index_u], self.topics[self.index_y]
279+
)
280+
281+
num_samples = len(t_sel)
282+
duration = t_sel[-1] - t_sel[0]
283+
284+
if num_samples < 256 or duration < 5:
285+
self.label_warning.setText(
286+
f"Increase the window size to at least 5 seconds and 256 samples. "
287+
f"Currently selected: {duration:.2f} seconds, {num_samples} samples."
288+
)
289+
self.label_warning.show()
290+
291+
self.coherence_ref.set_xdata([])
292+
self.coherence_ref.set_ydata([])
293+
return
294+
else:
295+
self.label_warning.hide()
296+
297+
# Estimate sampling frequency
298+
time_diffs = np.diff(t_sel)
299+
avg_time_diff = np.mean(time_diffs)
300+
if avg_time_diff == 0:
301+
return
302+
fs = 1 / avg_time_diff
303+
304+
# Choose segment size
305+
nperseg = min(1024, num_samples // 4)
306+
307+
# Compute coherence
308+
freq, Cuy = signal.coherence(u_sel, y_sel, fs, nperseg=nperseg)
309+
310+
# Update coherence plot
311+
self.coherence_ref.set_xdata(freq)
312+
self.coherence_ref.set_ydata(Cuy)
313+
self.ax_coherence.set_xlim([0, 20])
314+
self.ax_coherence.set_ylim([0, 1])
315+
316+
# Remove previous annotation if it exists
317+
if self.coherence_info_text is not None:
318+
self.coherence_info_text.remove()
319+
self.coherence_info_text = None
320+
321+
freq_res = fs / nperseg
322+
info_text = (f"Samples: {num_samples}, Duration: {duration:.2f}s, "
323+
f"fs: {fs:.1f}Hz, nperseg: {nperseg}, Δf: {freq_res:.2f}Hz")
324+
325+
self.coherence_info_text = self.ax_coherence.text(
326+
0.98, 0.02, info_text,
327+
ha='right', va='bottom',
328+
transform=self.ax_coherence.transAxes,
329+
fontsize=8, color='gray')
330+
331+
self.canvas.draw()
332+
333+
244334
def zoom_fun(self, event):
245335
base_scale = 1.1
246336
# get the current x and y limits

0 commit comments

Comments
 (0)