Source code for pyfda.plot_widgets.plot_tau_g

# -*- coding: utf-8 -*-
#
# This file is part of the pyFDA project hosted at https://github.com/chipmuenk/pyfda
#
# Copyright © pyFDA Project Contributors
# Licensed under the terms of the MIT License
# (see file LICENSE in root directory for details)

"""
Widget for plotting the group delay
"""
import logging
logger = logging.getLogger(__name__)

from pyfda.libs.compat import QCheckBox, QWidget, QFrame, QHBoxLayout, pyqtSignal, pyqtSlot

import numpy as np

import pyfda.filterbroker as fb
from pyfda.pyfda_rc import params
from scipy.signal import group_delay
from pyfda.plot_widgets.mpl_widget import MplWidget
from matplotlib.ticker import AutoMinorLocator

# TODO: Anticausal filter have no group delay. But is a filter with
#       'baA' always anticausal or maybe just acausal?

classes = {'Plot_tau_g':'tau_g'} #: Dict containing class name : display name

[docs]class Plot_tau_g(QWidget): """ Widget for plotting the group delay """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self, parent): super(Plot_tau_g, self).__init__(parent) self.verbose = True # suppress warnings self.needs_calc = True # flag whether plot needs to be recalculated self.tool_tip = "Group delay" self.tab_label = "\U0001D70F(f)"#"tau_g" \u03C4 self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements (currently commented out) """ # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # self.chkScipy = QCheckBox("Scipy", self) # self.chkScipy.setChecked(False) # self.chkScipy.setToolTip("Use scipy group delay routine") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # ============================================================================= self.mplwidget = MplWidget(self) #self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------
[docs] def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_calc = {1}, visible = {2}"\ .format(dict_sig, self.needs_calc, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False elif 'view_changed' in dict_sig: self.update_view() else: if 'data_changed' in dict_sig or 'view_changed' in dict_sig: self.needs_calc = True
#------------------------------------------------------------------------------
[docs] def init_axes(self): """ Initialize the axes and set some stuff that is not cleared by `ax.clear()` later on. """ self.ax = self.mplwidget.fig.subplots() self.ax.xaxis.tick_bottom() # remove axis ticks on top self.ax.yaxis.tick_left() # remove axis ticks right
#------------------------------------------------------------------------------
[docs] def calc_tau_g(self): """ (Re-)Calculate the complex frequency response H(f) """ bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: self.W, self.tau_g = group_delay((bb, aa), w=params['N_FFT'], whole = True) #verbose = self.verbose) # self.chkWarnings.isChecked()) # Zero phase filters have no group delay (Causal+AntiCausal) if 'baA' in fb.fil[0]: self.tau_g = np.zeros(self.tau_g.size)
#------------------------------------------------------------------------------ def draw(self): self.calc_tau_g() self.update_view() #------------------------------------------------------------------------------
[docs] def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c f_max_2 = fb.fil[0]['f_max'] / 2. F = self.W * f_max_2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift tau_g and F by f_S/2 tau_g = np.fft.fftshift(self.tau_g) F -= f_max_2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F tau_g = self.tau_g[0:params['N_FFT']//2] F = F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated tau_g = self.tau_g #================ Main Plotting Routine ========================= #=== clear the axes and (re)draw the plot if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $' else: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\ + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $' tau_g = tau_g / fb.fil[0]['f_S'] #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_tau_g, = self.ax.plot(F, tau_g, label = "Group Delay") #--------------------------------------------------------- self.ax.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax.yaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax.set_title(r'Group Delay $ \tau_g$') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(tau_str) # widen y-limits to suppress numerical inaccuracies when tau_g = constant self.ax.set_ylim([max(min(tau_g)-0.5,0), max(tau_g) + 0.5]) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw()
#------------------------------------------------------------------------------
[docs] def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
#------------------------------------------------------------------------------ def main(): import sys from pyfda.libs.compat import QApplication app = QApplication(sys.argv) mainw = Plot_tau_g(None) app.setActiveWindow(mainw) mainw.show() sys.exit(app.exec_()) if __name__ == "__main__": main() # module test using python -m pyfda.plot_widgets.plot_tau_g