Source code for pyfda.input_widgets.input_info

# -*- 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 displaying infos about filter and filter design method and debugging infos as well
"""
import sys
import pprint
import textwrap
import logging
logger = logging.getLogger(__name__)

from pyfda.libs.compat import (QtGui, QWidget, QFont, QFrame, QPushButton,
                      QTableWidget, QTableWidgetItem, QTextBrowser, QTextCursor,
                      QVBoxLayout, QHBoxLayout, QSplitter, Qt, pyqtSignal)

import numpy as np
from numpy import pi, log10
import scipy.signal as sig

import pyfda.filterbroker as fb # importing filterbroker initializes all its globals
import pyfda.filter_factory as ff # importing filterbroker initializes all its globals
from pyfda.libs.pyfda_lib import lin2unit, mod_version
from pyfda.input_widgets.input_info_about import AboutWindow#about_window
from pyfda.pyfda_rc import params
# TODO: Passband and stopband info should show min / max values for each band

if mod_version('docutils') is not None:
    from docutils.core import publish_string
    HAS_DOCUTILS = True
else:
    HAS_DOCUTILS = False

classes = {'Input_Info':'Info'} #: Dict containing class name : display name

[docs]class Input_Info(QWidget): """ Create widget for displaying infos about filter specs and filter design method """ sig_rx = pyqtSignal(object) # incoming signals from input_tab_widgets def __init__(self, parent): super(Input_Info, self).__init__(parent) self.tab_label = 'Info' self.tool_tip = ("<span>Display the achieved filter specifications" " and more info about the filter design algorithm.</span>") self._construct_UI() self.load_dict()
[docs] def process_sig_rx(self, dict_sig=None): """ Process signals coming from sig_rx """ logger.debug("Processing {0}: {1}".format(type(dict_sig).__name__, dict_sig)) if 'data_changed' in dict_sig or 'view_changed' in dict_sig or 'specs_changed' in dict_sig: self.load_dict()
def _construct_UI(self): """ Intitialize the widget, consisting of: - Checkboxes for selecting the info to be displayed - A large text window for displaying infos about the filter design algorithm """ bfont = QFont() bfont.setBold(True) # ============== UI Layout ===================================== # widget / subwindow for filter infos # self.butFiltPerf = QToolButton("H(f)", self) self.butFiltPerf = QPushButton(self) self.butFiltPerf.setText("H(f)") self.butFiltPerf.setCheckable(True) self.butFiltPerf.setChecked(True) self.butFiltPerf.setToolTip("Display frequency response at test frequencies.") self.butDebug = QPushButton(self) self.butDebug.setText("Debug") self.butDebug.setCheckable(True) self.butDebug.setChecked(False) self.butDebug.setToolTip("Show debugging options.") self.butAbout = QPushButton("About", self) # pop-up "About" window #self.butLic = QPushButton("License", self) # pop-up Licensing info layHControls1 = QHBoxLayout() layHControls1.addWidget(self.butFiltPerf) #layHControls1.addStretch(1) layHControls1.addWidget(self.butAbout) #layHControls1.addStretch(1) #layHControls1.addWidget(self.butLic) #layHControls1.addStretch(1) layHControls1.addWidget(self.butDebug) self.butDocstring = QPushButton("Doc$", self) self.butDocstring.setCheckable(True) self.butDocstring.setChecked(False) self.butDocstring.setToolTip("Display docstring from python filter method.") self.butRichText = QPushButton("RTF", self) self.butRichText.setCheckable(HAS_DOCUTILS) self.butRichText.setChecked(HAS_DOCUTILS) self.butRichText.setEnabled(HAS_DOCUTILS) self.butRichText.setToolTip("Render documentation in Rich Text Format.") self.butFiltDict = QPushButton("FiltDict", self) self.butFiltDict.setToolTip("Show filter dictionary for debugging.") self.butFiltDict.setCheckable(True) self.butFiltDict.setChecked(False) self.butFiltTree = QPushButton("FiltTree", self) self.butFiltTree.setToolTip("Show filter tree for debugging.") self.butFiltTree.setCheckable(True) self.butFiltTree.setChecked(False) layHControls2 = QHBoxLayout() layHControls2.addWidget(self.butDocstring) #layHControls2.addStretch(1) layHControls2.addWidget(self.butRichText) #layHControls2.addStretch(1) layHControls2.addWidget(self.butFiltDict) #layHControls2.addStretch(1) layHControls2.addWidget(self.butFiltTree) self.frmControls2 = QFrame(self) self.frmControls2.setLayout(layHControls2) self.frmControls2.setVisible(self.butDebug.isChecked()) self.frmControls2.setContentsMargins(0,0,0,0) #self.frmControls2.set layVControls = QVBoxLayout() layVControls.addLayout(layHControls1) layVControls.addWidget(self.frmControls2) self.frmMain = QFrame(self) self.frmMain.setLayout(layVControls) self.tblFiltPerf = QTableWidget(self) self.tblFiltPerf.setAlternatingRowColors(True) # self.tblFiltPerf.verticalHeader().setVisible(False) self.tblFiltPerf.horizontalHeader().setHighlightSections(False) self.tblFiltPerf.horizontalHeader().setFont(bfont) self.tblFiltPerf.verticalHeader().setHighlightSections(False) self.tblFiltPerf.verticalHeader().setFont(bfont) self.txtFiltInfoBox = QTextBrowser(self) self.txtFiltDict = QTextBrowser(self) self.txtFiltTree = QTextBrowser(self) layVMain = QVBoxLayout() layVMain.addWidget(self.frmMain) # layVMain.addLayout(self.layHControls) splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.tblFiltPerf) splitter.addWidget(self.txtFiltInfoBox) splitter.addWidget(self.txtFiltDict) splitter.addWidget(self.txtFiltTree) # setSizes uses absolute pixel values, but can be "misused" by specifying values # that are way too large: in this case, the space is distributed according # to the _ratio_ of the values: splitter.setSizes([3000,10000,1000,1000]) layVMain.addWidget(splitter) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.butFiltPerf.clicked.connect(self._show_filt_perf) self.butAbout.clicked.connect(self._about_window) self.butDebug.clicked.connect(self._show_debug) self.butFiltDict.clicked.connect(self._show_filt_dict) self.butFiltTree.clicked.connect(self._show_filt_tree) self.butDocstring.clicked.connect(self._show_doc) self.butRichText.clicked.connect(self._show_doc) def _about_window(self): self.about_widget = AboutWindow(self) # important: Handle must be class attribute #self.opt_widget.show() # modeless dialog, i.e. non-blocking self.about_widget.exec_() # modal dialog (blocking) #------------------------------------------------------------------------------ def _show_debug(self): """ Show / hide debug options depending on the state of the debug button """ self.frmControls2.setVisible(self.butDebug.isChecked()) #------------------------------------------------------------------------------
[docs] def load_dict(self): """ update docs and filter performance """ self._show_doc() self._show_filt_perf() self._show_filt_dict() self._show_filt_tree()
#------------------------------------------------------------------------------ def _show_doc(self): """ Display info from filter design file and docstring """ if hasattr(ff.fil_inst,'info'): if self.butRichText.isChecked(): self.txtFiltInfoBox.setText(publish_string( self._clean_doc(ff.fil_inst.info), writer_name='html', settings_overrides={'output_encoding': 'unicode'})) else: self.txtFiltInfoBox.setText(textwrap.dedent(ff.fil_inst.info)) else: self.txtFiltInfoBox.setText("") if self.butDocstring.isChecked() and hasattr(ff.fil_inst,'info_doc'): if self.butRichText.isChecked(): self.txtFiltInfoBox.append( '<hr /><b>Python module docstring:</b>\n') for doc in ff.fil_inst.info_doc: self.txtFiltInfoBox.append(publish_string( self._clean_doc(doc), writer_name='html', settings_overrides = {'output_encoding': 'unicode'})) else: self.txtFiltInfoBox.append('\nPython module docstring:\n') for doc in ff.fil_inst.info_doc: self.txtFiltInfoBox.append(self._clean_doc(doc)) self.txtFiltInfoBox.moveCursor(QTextCursor.Start) def _clean_doc(self, doc): """ Remove uniform number of leading blanks from docstrings for subsequent processing of rich text. The first line is treated differently, _all_ leading blanks are removed (if any). This allows for different formats of docstrings. """ lines = doc.splitlines() result = lines[0].lstrip() + "\n" + textwrap.dedent("\n".join(lines[1:])) return result #------------------------------------------------------------------------------ def _show_filt_perf(self): """ Print filter properties in a table at frequencies of interest. When specs are violated, colour the table entry in red. """ antiC = False def _find_min_max(self, f_start, f_stop, unit = 'dB'): """ Find minimum and maximum magnitude and the corresponding frequencies for the filter defined in the filter dict in a given frequency band [f_start, f_stop]. """ w = np.linspace(f_start, f_stop, params['N_FFT'])*2*np.pi [w, H] = sig.freqz(bb, aa, worN = w) # add antiCausals if we have them if (antiC): # # Evaluate transfer function of anticausal half on the same freq grid. # wa, ha = sig.freqz(bbA, aaA, worN = w) ha = ha.conjugate() # # Total transfer function is the product # H = H*ha f = w / (2.0 * pi) # frequency normalized to f_S H_abs = abs(H) H_max = max(H_abs) H_min = min(H_abs) F_max = f[np.argmax(H_abs)] # find the frequency where H_abs F_min = f[np.argmin(H_abs)] # becomes max resp. min if unit == 'dB': H_max = 20*log10(H_max) H_min = 20*log10(H_min) return F_min, H_min, F_max, H_max #------------------------------------------------------------------ self.tblFiltPerf.setVisible(self.butFiltPerf.isChecked()) if self.butFiltPerf.isChecked(): bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] #'rpk' means nonCausal filter if 'rpk' in fb.fil[0]: antiC = True bbA = fb.fil[0]['baA'][0] aaA = fb.fil[0]['baA'][1] bbA = bbA.conjugate() aaA = aaA.conjugate() f_S = fb.fil[0]['f_S'] f_lbls = [] f_vals = [] a_lbls = [] a_targs = [] a_targs_dB = [] a_test = [] ft = fb.fil[0]['ft'] # get filter type ('IIR', 'FIR') unit = fb.fil[0]['amp_specs_unit'] unit = 'dB' # fix this for the moment # construct pairs of corner frequency and corresponding amplitude # labels in ascending frequency for each response type if fb.fil[0]['rt'] in {'LP', 'HP', 'BP', 'BS', 'HIL'}: if fb.fil[0]['rt'] == 'LP': f_lbls = ['F_PB', 'F_SB'] a_lbls = ['A_PB', 'A_SB'] elif fb.fil[0]['rt'] == 'HP': f_lbls = ['F_SB', 'F_PB'] a_lbls = ['A_SB', 'A_PB'] elif fb.fil[0]['rt'] == 'BP': f_lbls = ['F_SB', 'F_PB', 'F_PB2', 'F_SB2'] a_lbls = ['A_SB', 'A_PB', 'A_PB', 'A_SB2'] elif fb.fil[0]['rt'] == 'BS': f_lbls = ['F_PB', 'F_SB', 'F_SB2', 'F_PB2'] a_lbls = ['A_PB', 'A_SB', 'A_SB', 'A_PB2'] elif fb.fil[0]['rt'] == 'HIL': f_lbls = ['F_PB', 'F_PB2'] a_lbls = ['A_PB', 'A_PB'] # Try to get lists of frequency / amplitude specs from the filter dict # that correspond to the f_lbls / a_lbls pairs defined above # When one of the labels doesn't exist in the filter dict, delete # all corresponding amplitude and frequency entries. err = [False] * len(f_lbls) # initialize error list f_vals = [] a_targs = [] for i in range(len(f_lbls)): try: f = fb.fil[0][f_lbls[i]] f_vals.append(f) except KeyError as e: f_vals.append('') err[i] = True logger.debug(e) try: a = fb.fil[0][a_lbls[i]] a_dB = lin2unit(fb.fil[0][a_lbls[i]], ft, a_lbls[i], unit) a_targs.append(a) a_targs_dB.append(a_dB) except KeyError as e: a_targs.append('') a_targs_dB.append('') err[i] = True logger.debug(e) for i in range(len(f_lbls)): if err[i]: del f_lbls[i] del f_vals[i] del a_lbls[i] del a_targs[i] del a_targs_dB[i] f_vals = np.asarray(f_vals) # convert to numpy array logger.debug("F_test_labels = %s" %f_lbls) # Calculate frequency response at test frequencies [w_test, a_test] = sig.freqz(bb, aa, 2.0 * pi * f_vals.astype(np.float)) # add antiCausals if we have them if (antiC): wa, ha = sig.freqz(bbA, aaA, 2.0 * pi * f_vals.astype(np.float)) ha = ha.conjugate() a_test = a_test*ha (F_min, H_min, F_max, H_max) = _find_min_max(self, 0, 1, unit = 'V') # append frequencies and values for min. and max. filter reponse to # test vector f_lbls += ['Min.','Max.'] # QTableView does not support direct formatting, use QLabel f_vals = np.append(f_vals, [F_min, F_max]) a_targs = np.append(a_targs, [np.nan, np.nan]) a_targs_dB = np.append(a_targs_dB, [np.nan, np.nan]) a_test = np.append(a_test, [H_min, H_max]) # calculate response of test frequencies in dB a_test_dB = -20*log10(abs(a_test)) ft = fb.fil[0]['ft'] # get filter type ('IIR', 'FIR') for dB <-> lin conversion # unit = fb.fil[0]['amp_specs_unit'] unit = 'dB' # make this fixed for the moment # build a list with the corresponding target specs: a_targs_pass = [] eps = 1e-3 for i in range(len(f_lbls)): if 'PB' in f_lbls[i]: a_targs_pass.append((a_test_dB[i] - a_targs_dB[i])< eps) a_test[i] = 1 - abs(a_test[i]) elif 'SB' in f_lbls[i]: a_targs_pass.append(a_test_dB[i] >= a_targs_dB[i]) else: a_targs_pass.append(True) self.targs_spec_passed = np.all(a_targs_pass) logger.debug("H_targ = {0}\n" "H_test = {1}\n" "H_test_dB = {2}\n" "F_test = {3}\n" "H_targ_pass = {4}\n" "passed: {5}\n".format(a_targs, a_test, a_test_dB, f_vals, a_targs_pass,self.targs_spec_passed)) self.tblFiltPerf.setRowCount(len(a_test)) # number of table rows self.tblFiltPerf.setColumnCount(5) # number of table columns self.tblFiltPerf.setHorizontalHeaderLabels([ 'f/{0:s}'.format(fb.fil[0]['freq_specs_unit']),'Spec\n(dB)', '|H(f)|\n(dB)', 'Spec', '|H(f)|'] ) self.tblFiltPerf.setVerticalHeaderLabels(f_lbls) for row in range(len(a_test)): self.tblFiltPerf.setItem(row,0,QTableWidgetItem(str('{0:.4g}'.format(f_vals[row]*f_S)))) self.tblFiltPerf.setItem(row,1,QTableWidgetItem(str('%2.3g'%(-a_targs_dB[row])))) self.tblFiltPerf.setItem(row,2,QTableWidgetItem(str('%2.3f'%(-a_test_dB[row])))) if a_targs[row] < 0.01: self.tblFiltPerf.setItem(row,3,QTableWidgetItem(str('%.3e'%(a_targs[row])))) else: self.tblFiltPerf.setItem(row,3,QTableWidgetItem(str('%2.4f'%(a_targs[row])))) if a_test[row] < 0.01: self.tblFiltPerf.setItem(row,4,QTableWidgetItem(str('%.3e'%(abs(a_test[row]))))) else: self.tblFiltPerf.setItem(row,4,QTableWidgetItem(str('%.4f'%(abs(a_test[row]))))) if not a_targs_pass[row]: self.tblFiltPerf.item(row,1).setBackground(QtGui.QColor('red')) self.tblFiltPerf.item(row,3).setBackground(QtGui.QColor('red')) self.tblFiltPerf.resizeColumnsToContents() self.tblFiltPerf.resizeRowsToContents() #------------------------------------------------------------------------------ def _show_filt_dict(self): """ Print filter dict for debugging """ self.txtFiltDict.setVisible(self.butFiltDict.isChecked()) fb_sorted = [str(key) +' : '+ str(fb.fil[0][key]) for key in sorted(fb.fil[0].keys())] dictstr = pprint.pformat(fb_sorted) # dictstr = pprint.pformat(fb.fil[0]) self.txtFiltDict.setText(dictstr) #------------------------------------------------------------------------------ def _show_filt_tree(self): """ Print filter tree for debugging """ self.txtFiltTree.setVisible(self.butFiltTree.isChecked()) ftree_sorted = ['<b>' + str(key) +' : '+ '</b>' + str(fb.fil_tree[key]) for key in sorted(fb.fil_tree.keys())] dictstr = pprint.pformat(ftree_sorted, indent = 4) # dictstr = pprint.pformat(fb.fil[0]) self.txtFiltTree.setText(dictstr)
#------------------------------------------------------------------------------ if __name__ == '__main__': """ Test with python -m pyfda.input_widgets.input_info""" from pyfda.libs.compat import QApplication from pyfda import pyfda_rc as rc app = QApplication(sys.argv) app.setStyleSheet(rc.qss_rc) mainw = Input_Info(None) app.setActiveWindow(mainw) mainw.show() sys.exit(app.exec_())