5
5
from matplotlib .widgets import SpanSelector
6
6
7
7
import numpy as np
8
+ from scipy import signal
8
9
9
10
from data_extractor import DataExtractor
10
11
from searchable_combo_box import SearchableComboBox
@@ -56,9 +57,9 @@ def __init__(self, filename):
56
57
57
58
self .input_ref = None
58
59
self .output_ref = None
59
- self .figure = plt .figure (1 )
60
+ self .figure = plt .figure (figsize = ( 8 , 6 ) )
60
61
self .canvas = FigureCanvas (self .figure )
61
- self .initPlot ()
62
+ self .initPlots ()
62
63
63
64
layout_v = QVBoxLayout ()
64
65
@@ -183,10 +184,12 @@ def selectYData(self, index):
183
184
def getTrimAirspeed (self ):
184
185
return self .data_extractor .getTrimAirspeed ()
185
186
186
- def initPlot (self ):
187
+ def initPlots (self ):
187
188
if self .input_ref is None :
188
189
self .figure .clear ()
189
- self .ax = self .figure .add_subplot (1 ,1 ,1 )
190
+
191
+ # --- Time series Axes (Top) ---
192
+ self .ax = self .figure .add_subplot (2 ,1 ,1 )
190
193
color_in = 'tab:blue'
191
194
plot_refs = self .ax .plot ([], [], color = color_in )
192
195
self .input_ref = plot_refs [0 ]
@@ -207,6 +210,15 @@ def initPlot(self):
207
210
208
211
self .span = SpanSelector (self .ax_out , self .onselect , 'horizontal' , useblit = False ,
209
212
props = dict (alpha = 0.2 , facecolor = 'green' ), interactive = True )
213
+
214
+ # --- Coherence Plot (Bottom) ---
215
+ self .ax_coherence = self .figure .add_subplot (2 , 1 , 2 )
216
+ self .ax_coherence .set_title ("Coherence" )
217
+ self .ax_coherence .set_xlabel ("Frequency [Hz]" )
218
+ self .ax_coherence .set_ylabel ("Coherence" )
219
+ self .coherence_ref , = self .ax_coherence .plot ([], [])
220
+
221
+ self .figure .tight_layout ()
210
222
211
223
self .canvas .mpl_connect ('scroll_event' , self .zoom_fun )
212
224
self .canvas .draw ()
@@ -231,6 +243,71 @@ def plotY(self):
231
243
self .ax_out .set_ylim ([min_y , max_y ])
232
244
self .canvas .draw ()
233
245
246
+ def plotCoherence (self ):
247
+ if len (self .t ) == 0 or len (self .u ) == 0 or len (self .y ) == 0 :
248
+ return
249
+
250
+ # Use selected range if available
251
+ if self .t_start is not None and self .t_stop is not None and self .t_stop > self .t_start :
252
+ # Get indices within selected range
253
+ ind_start = np .searchsorted (self .t , self .t_start )
254
+ ind_stop = np .searchsorted (self .t , self .t_stop )
255
+
256
+ t_sel = self .t [ind_start :ind_stop ]
257
+ u_sel = self .u [ind_start :ind_stop ]
258
+ y_sel = self .y [ind_start :ind_stop ]
259
+ else :
260
+ # Fall back to full signal if no range selected
261
+ t_sel = self .t
262
+ u_sel = self .u
263
+ y_sel = self .y
264
+
265
+ num_samples = len (t_sel )
266
+ if num_samples < 64 : # I kind of made up this number -> maybe requires some research
267
+ self .ax_coherence .clear ()
268
+ self .ax_coherence .set_title ("Coherence (Selection too short)" )
269
+ self .ax_coherence .text (0.5 , 0.5 , f"Not enough data ({ num_samples } samples).\n Select a larger window." ,
270
+ ha = 'center' , va = 'center' , transform = self .ax_coherence .transAxes ,
271
+ fontsize = 10 , color = 'red' )
272
+ self .canvas .draw ()
273
+ return
274
+
275
+ # Estimate sampling frequency
276
+ time_diffs = np .diff (t_sel )
277
+ avg_time_diff = np .mean (time_diffs )
278
+ if avg_time_diff == 0 :
279
+ return
280
+ fs = 1 / avg_time_diff
281
+
282
+ # Choose segment size
283
+ nperseg = min (256 , num_samples // 4 ) # Also needs to be verified
284
+
285
+ # Compute coherence
286
+ freq , Cuy = signal .coherence (u_sel , y_sel , fs , nperseg = nperseg )
287
+
288
+ # Update coherence plot
289
+ self .ax_coherence .clear ()
290
+ self .ax_coherence .plot (freq , Cuy , label = 'Coherence(u, y)' )
291
+ self .ax_coherence .set_title ("Coherence" )
292
+ self .ax_coherence .set_xlabel ("Frequency [Hz]" )
293
+ self .ax_coherence .set_ylabel ("Coherence" )
294
+ self .ax_coherence .set_xlim ([min (freq ), max (freq )])
295
+ self .ax_coherence .set_ylim ([0 , 1 ])
296
+ self .ax_coherence .grid (True )
297
+
298
+ # Add informative annotation
299
+ duration = t_sel [- 1 ] - t_sel [0 ]
300
+ freq_res = fs / nperseg
301
+ info_text = (f"Samples: { num_samples } , Duration: { duration :.2f} s, "
302
+ f"fs: { fs :.1f} Hz, nperseg: { nperseg } , Δf: { freq_res :.2f} Hz" )
303
+
304
+ self .ax_coherence .text (0.98 , 0.02 , info_text ,
305
+ ha = 'right' , va = 'bottom' ,
306
+ transform = self .ax_coherence .transAxes ,
307
+ fontsize = 8 , color = 'gray' )
308
+
309
+ self .canvas .draw ()
310
+
234
311
def onselect (self , xmin , xmax ):
235
312
indmin , indmax = np .searchsorted (self .t , (xmin , xmax ))
236
313
indmax = min (len (self .t ) - 1 , indmax )
@@ -241,6 +318,8 @@ def onselect(self, xmin, xmax):
241
318
self .ax .set_xlim (self .t_start - 1.0 , self .t_stop + 1.0 )
242
319
self .canvas .draw ()
243
320
321
+ self .plotCoherence ()
322
+
244
323
def zoom_fun (self , event ):
245
324
base_scale = 1.1
246
325
# get the current x and y limits
0 commit comments