55from matplotlib .widgets import SpanSelector
66
77import numpy as np
8+ from scipy import signal
89
910from data_extractor import DataExtractor
1011from searchable_combo_box import SearchableComboBox
@@ -56,9 +57,9 @@ def __init__(self, filename):
5657
5758 self .input_ref = None
5859 self .output_ref = None
59- self .figure = plt .figure (1 )
60+ self .figure = plt .figure (figsize = ( 8 , 6 ) )
6061 self .canvas = FigureCanvas (self .figure )
61- self .initPlot ()
62+ self .initPlots ()
6263
6364 layout_v = QVBoxLayout ()
6465
@@ -183,10 +184,12 @@ def selectYData(self, index):
183184 def getTrimAirspeed (self ):
184185 return self .data_extractor .getTrimAirspeed ()
185186
186- def initPlot (self ):
187+ def initPlots (self ):
187188 if self .input_ref is None :
188189 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 )
190193 color_in = 'tab:blue'
191194 plot_refs = self .ax .plot ([], [], color = color_in )
192195 self .input_ref = plot_refs [0 ]
@@ -207,6 +210,15 @@ def initPlot(self):
207210
208211 self .span = SpanSelector (self .ax_out , self .onselect , 'horizontal' , useblit = False ,
209212 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 ()
210222
211223 self .canvas .mpl_connect ('scroll_event' , self .zoom_fun )
212224 self .canvas .draw ()
@@ -231,6 +243,71 @@ def plotY(self):
231243 self .ax_out .set_ylim ([min_y , max_y ])
232244 self .canvas .draw ()
233245
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+
234311 def onselect (self , xmin , xmax ):
235312 indmin , indmax = np .searchsorted (self .t , (xmin , xmax ))
236313 indmax = min (len (self .t ) - 1 , indmax )
@@ -241,6 +318,8 @@ def onselect(self, xmin, xmax):
241318 self .ax .set_xlim (self .t_start - 1.0 , self .t_stop + 1.0 )
242319 self .canvas .draw ()
243320
321+ self .plotCoherence ()
322+
244323 def zoom_fun (self , event ):
245324 base_scale = 1.1
246325 # get the current x and y limits
0 commit comments