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 sig_tx = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): 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() 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.butSettings = QPushButton("Settings", self) # self.butSettings.setCheckable(True) self.butSettings.setChecked(False) self.butSettings.setToolTip("Display and set some settings") layHControls1 = QHBoxLayout() layHControls1.addWidget(self.butFiltPerf) layHControls1.addWidget(self.butAbout) layHControls1.addWidget(self.butSettings) 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) lbl_settings_NFFT = QLabel(to_html("N_FFT =", frmt='bi'), self) self.led_settings_NFFT = QLineEdit(self) self.led_settings_NFFT.setText(str(params['N_FFT'])) self.led_settings_NFFT.setToolTip("<span>Number of FFT points for frequency " "domain widgets.</span>") layGSettings = QGridLayout() layGSettings.addWidget(lbl_settings_NFFT, 1, 0) layGSettings.addWidget(self.led_settings_NFFT, 1, 1) self.frmSettings = QFrame(self) self.frmSettings.setLayout(layGSettings) self.frmSettings.setVisible(self.butSettings.isChecked()) self.frmSettings.setContentsMargins(0, 0, 0, 0) layVControls = QVBoxLayout() layVControls.addLayout(layHControls1) layVControls.addWidget(self.frmControls2) layVControls.addWidget(self.frmSettings) 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.butSettings.clicked.connect(self._show_settings) self.led_settings_NFFT.editingFinished.connect(self._update_settings_nfft) 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()) # ------------------------------------------------------------------------------ def _show_settings(self): """ Show / hide settings options depending on the state of the settings button """ self.frmSettings.setVisible(self.butSettings.isChecked()) def _update_settings_nfft(self): """ Update value for self.par1 from QLineEdit Widget""" params['N_FFT'] = safe_eval(self.led_settings_NFFT.text(), params['N_FFT'], sign='pos', return_type='int') self.led_settings_NFFT.setText(str(params['N_FFT'])) self.emit({'data_changed': 'n_fft'}) # ------------------------------------------------------------------------------ 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(float)) # add antiCausals if we have them if (antiC): wa, ha = sig.freqz(bbA, aaA, 2.0 * pi * f_vals.astype(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)) # get filter type ('IIR', 'FIR') for dB <-> lin conversion ft = fb.fil[0]['ft'] # 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)
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.butSettings = QPushButton("Settings", self) # self.butSettings.setCheckable(True) self.butSettings.setChecked(False) self.butSettings.setToolTip("Display and set some settings") layHControls1 = QHBoxLayout() layHControls1.addWidget(self.butFiltPerf) layHControls1.addWidget(self.butAbout) layHControls1.addWidget(self.butSettings) 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) lbl_settings_NFFT = QLabel(to_html("N_FFT =", frmt='bi'), self) self.led_settings_NFFT = QLineEdit(self) self.led_settings_NFFT.setText(str(params['N_FFT'])) self.led_settings_NFFT.setToolTip("<span>Number of FFT points for frequency " "domain widgets.</span>") layGSettings = QGridLayout() layGSettings.addWidget(lbl_settings_NFFT, 1, 0) layGSettings.addWidget(self.led_settings_NFFT, 1, 1) self.frmSettings = QFrame(self) self.frmSettings.setLayout(layGSettings) self.frmSettings.setVisible(self.butSettings.isChecked()) self.frmSettings.setContentsMargins(0, 0, 0, 0) layVControls = QVBoxLayout() layVControls.addLayout(layHControls1) layVControls.addWidget(self.frmControls2) layVControls.addWidget(self.frmSettings) 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.butSettings.clicked.connect(self._show_settings) self.led_settings_NFFT.editingFinished.connect(self._update_settings_nfft) 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)
class AboutWindow(QDialog): """ Create a pop-up widget for the About Window. """ # sig_tx = pyqtSignal(dict) # outgoing def __init__(self, parent): super(AboutWindow, self).__init__(parent) self.setWindowTitle("About pyFDA") self.collect_info() self._construct_UI() qwindow_stay_on_top(self, True) #------------------------------------------------------------------------------ def _construct_UI(self): """ initialize the User Interface """ butClipboard = QPushButton(self) butClipboard.setIcon(QIcon(':/clipboard.svg')) butClipboard.setToolTip("Copy text to clipboard.") butAbout = QPushButton(self) butAbout.setText("About") butAbout.setToolTip("Display 'About' info") butChangelog = QPushButton(self) butChangelog.setText("Changelog") butChangelog.setToolTip("Display changelog") butLicMIT = QPushButton(self) butLicMIT.setText("MIT License") butLicMIT.setToolTip("MIT License for pyFDA source code") butLicGPLv3 = QPushButton(self) butLicGPLv3.setText("GPLv3 License") butLicGPLv3.setToolTip("GPLv3 License for bundled distribution") butClose = QPushButton(self) butClose.setIcon(QIcon(':/circle-x.svg')) butClose.setToolTip("Close Window.") layGButtons = QGridLayout() layGButtons.addWidget(butClipboard, 0, 0) layGButtons.addWidget(butAbout, 0, 1) layGButtons.addWidget(butChangelog, 0, 2) layGButtons.addWidget(butLicMIT, 0, 3) layGButtons.addWidget(butLicGPLv3, 0, 4) layGButtons.addWidget(butClose, 0, 5) lblInfo = QLabel(self) lblInfo.setText(self.info_str) lblInfo.setFixedHeight(lblInfo.height() * 1.2) #lblInfo.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) lblInfo.setOpenExternalLinks(True) lblIcon = QLabel(self) lblIcon.setPixmap( QPixmap(':/pyfda_icon.svg').scaledToHeight( lblInfo.height(), Qt.SmoothTransformation)) butClipboard.setFixedWidth(lblInfo.height()) butClose.setFixedWidth(lblInfo.height()) layHInfo = QHBoxLayout() layHInfo.addWidget(lblIcon) layHInfo.addWidget(lblInfo) self.txtDisplay = QTextBrowser(self) self.txtDisplay.setOpenExternalLinks(True) self.display_about_str() self.txtDisplay.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #self.txtDisplay.setFixedHeight(self.txtDisplay.width() * 2) layVMain = QVBoxLayout() # layVMain.setAlignment(Qt.AlignTop) # this affects only the first widget (intended here) layVMain.addLayout(layGButtons) layVMain.addLayout(layHInfo) layVMain.addWidget(self.txtDisplay) layVMain.setContentsMargins(*params['wdg_margins_spc']) self.setLayout(layVMain) #self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #self.resize(0,0) #self.adjustSize() #QApplication.processEvents() butClipboard.clicked.connect( lambda: self.to_clipboard(self.info_str + self.about_str)) butAbout.clicked.connect(self.display_about_str) butChangelog.clicked.connect(self.display_changelog) butLicMIT.clicked.connect(self.display_MIT_lic) butLicGPLv3.clicked.connect(self.display_GPL_lic) butClose.clicked.connect(self.close) #------------------------------------------------------------------------------ def to_clipboard(self, my_string): """ Copy version info to clipboard """ mapping = [ ('<br>', '\n'), ('<br />', '\n'), ('</tr>', '\n'), ('</th>', '\n==============\n'), ('</table>', '\n'), ('<hr>', '\n---------\n'), ('<b>', ''), ('</b>', ''), ('<tr>', ''), ('<td>', ''), ('</td>', '\t'), ('<th>', ''), (' ', ' '), ('<table>', ''), # ('</a>',''), ("<th style='font-size:large;'>", "\n") ] # ============================================================================= # """ Map some HTML tags to control codes""" # for k, v in mapping: # my_string = my_string.replace(k, v) # # """Remove html tags from a string""" # clean = re.compile('<.*?>') # fb.clipboard.setText(re.sub(clean, '', my_string)) # ============================================================================= fb.clipboard.setText(my_string) #------------------------------------------------------------------------------ def collect_info(self): """ Collect information about version, imported modules in strings: `self.info_str` : General info, copyright, version, link to readthedocs This info is always visible. `self.about_str`: OS, user name, directories, versions of installed software """ self.info_str = ("<b><a href=https://www.github.com/chipmuenk/pyfda>pyfda</a> " "Version {0} (c) 2013 - 2020 Christian Münker</b><br />" "Design, analyze and synthesize digital filters. Docs @ " "<a href=https://pyfda.rtfd.org>pyfda.rtfd.org</a>" " (<a href=https://media.readthedocs.org/pdf/pyfda/latest/pyfda.pdf>pdf</a>)<br />"\ .format(version.__version__)) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - user_dirs_str = "" if dirs.USER_DIRS: for d in dirs.USER_DIRS: user_dirs_str += d + '<br />' else: user_dirs_str = "None<br />" # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - os_str = ("<b>OS:</b> {0} {1}<br><b>User Name:</b> {2}".format( dirs.OS, dirs.OS_VER, dirs.USER_NAME)) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dirs_md = ("### Directories ###\n" "| *Function* | *Path*|\n"#"| <!-- --> | <!-- --> |\n" "|: ---- |: ---- |\n" "| **Install Dir ** | `{install_dir}` |\n" "| **User Module Dir ** | `{user_dir}` |\n" "| **Home Dir** | `{home_dir}` |\n" "| **Temp Dir ** | `{temp_dir}` |\n" "| **Config Dir ** | `{conf_dir}` |\n" "| - - - - - - - | - - - - - - - - -|\n" "| **pyFDA Config ** | `{pyfda_conf}` |\n" "| **Log. Config ** | `{log_conf}` |\n" "| **Logfile ** | `{log_file}` |"\ .format(home_dir=dirs.HOME_DIR, install_dir=dirs.INSTALL_DIR, conf_dir=dirs.CONF_DIR, user_dir=user_dirs_str[:-6], temp_dir=dirs.TEMP_DIR, pyfda_conf=dirs.USER_CONF_DIR_FILE, log_conf=dirs.USER_LOG_CONF_DIR_FILE, log_file=dirs.LOG_DIR_FILE)) dirs_str = markdown.markdown(dirs_md, output_format='html5', extensions=['tables']) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ver_str = pyfda_lib.mod_version() # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if False: #dirs.PYINSTALLER: self.lic_str = "" else: with open(os.path.join(dirs.INSTALL_DIR, "license_info.md"), 'r') as f: self.lic_str = markdown.markdown(f.read(), output_format='html5', extensions=['tables']) self.about_str = os_str + dirs_str + ver_str #------------------------------------------------------------------------------ def display_about_str(self): """ Display general "About" info """ self.txtDisplay.setText(self.about_str + self.lic_str) #------------------------------------------------------------------------------ def display_changelog(self): """ Display changelog """ with open(os.path.join(dirs.INSTALL_DIR, "..", "CHANGELOG.md"), 'r') as f: lic_str = markdown.markdown(f.read(), output_format='html5') self.txtDisplay.setText(lic_str) #------------------------------------------------------------------------------ def display_MIT_lic(self): """ Display MIT license """ with open(os.path.join(dirs.INSTALL_DIR, "..", "LICENSE.md"), 'r') as f: lic_str = markdown.markdown(f.read(), output_format='html5') self.txtDisplay.setText(lic_str) #------------------------------------------------------------------------------ def display_GPL_lic(self): """ Display MIT license """ with open(os.path.join(dirs.INSTALL_DIR, "..", "LICENSE_GPL3_0.html"), 'r') as f: lic_str = f.read() self.txtDisplay.setText(lic_str)
def _construct_UI(self): """ initialize the User Interface """ butClipboard = QPushButton(self) butClipboard.setIcon(QIcon(':/clipboard.svg')) butClipboard.setToolTip("Copy text to clipboard.") butAbout = QPushButton(self) butAbout.setText("About") butAbout.setToolTip("Display 'About' info") butChangelog = QPushButton(self) butChangelog.setText("Changelog") butChangelog.setToolTip("Display changelog") butLicMIT = QPushButton(self) butLicMIT.setText("MIT License") butLicMIT.setToolTip("MIT License for pyFDA source code") butLicGPLv3 = QPushButton(self) butLicGPLv3.setText("GPLv3 License") butLicGPLv3.setToolTip("GPLv3 License for bundled distribution") butClose = QPushButton(self) butClose.setIcon(QIcon(':/circle-x.svg')) butClose.setToolTip("Close Window.") layGButtons = QGridLayout() layGButtons.addWidget(butClipboard, 0, 0) layGButtons.addWidget(butAbout, 0, 1) layGButtons.addWidget(butChangelog, 0, 2) layGButtons.addWidget(butLicMIT, 0, 3) layGButtons.addWidget(butLicGPLv3, 0, 4) layGButtons.addWidget(butClose, 0, 5) lblInfo = QLabel(self) lblInfo.setText(self.info_str) lblInfo.setFixedHeight(lblInfo.height() * 1.2) #lblInfo.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) lblInfo.setOpenExternalLinks(True) lblIcon = QLabel(self) lblIcon.setPixmap( QPixmap(':/pyfda_icon.svg').scaledToHeight( lblInfo.height(), Qt.SmoothTransformation)) butClipboard.setFixedWidth(lblInfo.height()) butClose.setFixedWidth(lblInfo.height()) layHInfo = QHBoxLayout() layHInfo.addWidget(lblIcon) layHInfo.addWidget(lblInfo) self.txtDisplay = QTextBrowser(self) self.txtDisplay.setOpenExternalLinks(True) self.display_about_str() self.txtDisplay.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #self.txtDisplay.setFixedHeight(self.txtDisplay.width() * 2) layVMain = QVBoxLayout() # layVMain.setAlignment(Qt.AlignTop) # this affects only the first widget (intended here) layVMain.addLayout(layGButtons) layVMain.addLayout(layHInfo) layVMain.addWidget(self.txtDisplay) layVMain.setContentsMargins(*params['wdg_margins_spc']) self.setLayout(layVMain) #self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #self.resize(0,0) #self.adjustSize() #QApplication.processEvents() butClipboard.clicked.connect( lambda: self.to_clipboard(self.info_str + self.about_str)) butAbout.clicked.connect(self.display_about_str) butChangelog.clicked.connect(self.display_changelog) butLicMIT.clicked.connect(self.display_MIT_lic) butLicGPLv3.clicked.connect(self.display_GPL_lic) butClose.clicked.connect(self.close)
class Plot_FFT_win(QDialog): """ Create a pop-up widget for displaying time and frequency view of an FFT window. Data is passed via the dictionary `win_dict` that is passed during construction. """ # incoming sig_rx = pyqtSignal(object) # outgoing sig_tx = pyqtSignal(object) def __init__(self, parent, win_dict=fb.fil[0]['win_fft'], sym=True, title='pyFDA Window Viewer'): super(Plot_FFT_win, self).__init__(parent) self.needs_calc = True self.needs_draw = True self.needs_redraw = True self.bottom_f = -80 # min. value for dB display self.bottom_t = -60 self.N = 32 # initial number of data points self.N_auto = win_dict['win_len'] self.pad = 16 # amount of zero padding self.win_dict = win_dict self.sym = sym self.tbl_rows = 2 self.tbl_cols = 6 # initial settings for checkboxes self.tbl_sel = [True, True, False, False] self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(title) self._construct_UI() qwindow_stay_on_top(self, True) #------------------------------------------------------------------------------ def closeEvent(self, event): """ Catch closeEvent (user has tried to close the window) and send a signal to parent where window closing is registered before actually closing the window. """ self.sig_tx.emit({'sender': __name__, 'closeEvent': ''}) event.accept() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("PROCESS_SIG_RX - vis: {0}\n{1}"\ .format(self.isVisible(), pprint_log(dict_sig))) if ('view_changed' in dict_sig and dict_sig['view_changed'] == 'win')\ or ('filt_changed' in dict_sig and dict_sig['filt_changed'] == 'firwin')\ or self.needs_calc: # logger.warning("Auto: {0} - WinLen: {1}".format(self.N_auto, self.win_dict['win_len'])) self.N_auto = self.win_dict['win_len'] self.calc_N() if self.isVisible(): self.draw() self.needs_calc = False else: self.needs_calc = True elif 'home' in dict_sig: self.update_view() else: logger.error("Unknown content of dict_sig: {0}".format(dict_sig)) #------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.chk_auto_N = QCheckBox(self) self.chk_auto_N.setChecked(False) self.chk_auto_N.setToolTip( "Use number of points from calling routine.") self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N)) self.led_N.setMaximumWidth(70) self.led_N.setToolTip("<span>Number of window data points.</span>") self.chk_log_t = QCheckBox("Log", self) self.chk_log_t.setChecked(False) self.chk_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(50) self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel("dB", self) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.chk_norm_f = QCheckBox("Norm", self) self.chk_norm_f.setChecked(True) self.chk_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.chk_half_f = QCheckBox("Half", self) self.chk_half_f.setChecked(True) self.chk_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") self.chk_log_f = QCheckBox("Log", self) self.chk_log_f.setChecked(True) self.chk_log_f.setToolTip("Display in dB") self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(50) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_f = QLabel("dB", self) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) layHControls = QHBoxLayout() layHControls.addWidget(self.chk_auto_N) layHControls.addWidget(self.lbl_auto_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addStretch(10) layHControls.addWidget(self.chk_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_half_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.lbl_log_bottom_f) self.tblWinProperties = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tblWinProperties.setAlternatingRowColors(True) self.tblWinProperties.verticalHeader().setVisible(False) self.tblWinProperties.horizontalHeader().setVisible(False) self._init_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget: Layout layVMainMpl (VBox) is defined with MplWidget, # additional widgets can be added (like self.frmControls) # The widget encompasses all other widgets. #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. #---------------------------------------------------------------------- layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tblWinProperties) layVInfo.addWidget(self.txtInfoBox) self.frmInfo = QFrame(self) self.frmInfo.setObjectName("frmInfo") self.frmInfo.setLayout(layVInfo) #---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(self.frmInfo) # 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, 1000]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) #---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chk_log_f.clicked.connect(self.update_view) self.chk_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.chk_auto_N.clicked.connect(self.calc_N) self.led_N.editingFinished.connect(self.draw) self.chk_norm_f.clicked.connect(self.draw) self.chk_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tblWinProperties.itemClicked.connect(self._handle_item_clicked) #------------------------------------------------------------------------------ def _init_table(self, rows, cols, val): for r in range(rows): for c in range(cols): item = QTableWidgetItem(val) if c % 3 == 0: item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if self.tbl_sel[r * 2 + c % 3]: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self.tblWinProperties.setItem(r, c, item) # https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget #------------------------------------------------------------------------------ def _set_table_item(self, row, col, val, font=None, sel=None): """ Set the table item with the index `row, col` and the value val """ item = self.tblWinProperties.item(row, col) item.setText(str(val)) if font: self.tblWinProperties.item(row, col).setFont(font) if sel == True: item.setCheckState(Qt.Checked) if sel == False: item.setCheckState(Qt.Unchecked) # when sel is not specified, don't change anything #------------------------------------------------------------------------------ def _handle_item_clicked(self, item): if item.column() % 3 == 0: # clicked on checkbox num = item.row() * 2 + item.column() // 3 if item.checkState() == Qt.Checked: self.tbl_sel[num] = True logger.debug('"{0}:{1}" Checked'.format(item.text(), num)) else: self.tbl_sel[num] = False logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num)) elif item.column() % 3 == 1: # clicked on value field logger.info("{0:s} copied to clipboard.".format(item.text())) fb.clipboard.setText(item.text()) self.update_view() #------------------------------------------------------------------------------ def update_bottom(self): """ Update log bottom settings """ self.bottom_t = safe_eval(self.led_log_bottom_t.text(), self.bottom_t, sign='neg', return_type='float') self.led_log_bottom_t.setText(str(self.bottom_t)) self.bottom_f = safe_eval(self.led_log_bottom_f.text(), self.bottom_f, sign='neg', return_type='float') self.led_log_bottom_f.setText(str(self.bottom_f)) self.update_view() #------------------------------------------------------------------------------ def calc_N(self): """ (Re-)Calculate the number of data points when Auto N chkbox has been clicked or when the number of data points has been updated outside this class """ if self.chk_auto_N.isChecked(): self.N = self.N_auto self.draw() #------------------------------------------------------------------------------ def calc_win(self): """ (Re-)Calculate the window and its FFT """ self.led_N.setEnabled(not self.chk_auto_N.isChecked()) if not self.chk_auto_N.isChecked(): self.N = safe_eval(self.led_N.text(), self.N, sign='pos', return_type='int') # else: #self.N = self.win_dict['win_len'] self.led_N.setText(str(self.N)) self.n = np.arange(self.N) self.win = calc_window_function(self.win_dict, self.win_dict['name'], self.N, sym=self.sym) self.nenbw = self.N * np.sum(np.square(self.win)) / (np.square( np.sum(self.win))) self.cgain = np.sum(self.win) / self.N self.F = fftfreq(self.N * self.pad, d=1. / fb.fil[0]['f_S']) # use zero padding self.Win = np.abs(fft(self.win, self.N * self.pad)) if self.chk_norm_f.isChecked(): self.Win /= (self.N * self.cgain ) # correct gain for periodic signals (coherent gain) first_zero = argrelextrema(self.Win[:(self.N * self.pad) // 2], np.less) if np.shape(first_zero)[1] > 0: first_zero = first_zero[0][0] self.first_zero_f = self.F[first_zero] self.sidelobe_level = np.max( self.Win[first_zero:(self.N * self.pad) // 2]) else: self.first_zero_f = np.nan self.sidelobe_level = 0 #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate \|H(f)\| and draw the figure """ self.calc_win() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating the window. """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') self.ax_t.cla() self.ax_f.cla() self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$') self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$') if self.chk_log_t.isChecked(): self.ax_t.plot( self.n, np.maximum(20 * np.log10(np.abs(self.win)), self.bottom_t)) else: self.ax_t.plot(self.n, self.win) if self.chk_half_f.isChecked(): F = self.F[:len(self.F * self.pad) // 2] Win = self.Win[:len(self.F * self.pad) // 2] else: F = fftshift(self.F) Win = fftshift(self.Win) if self.chk_log_f.isChecked(): self.ax_f.plot( F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f)) self.nenbw_disp = 10 * np.log10(self.nenbw) self.cgain_disp = 20 * np.log10(self.cgain) self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level) self.unit_nenbw = "dB" self.unit_scale = "dB" else: self.ax_f.plot(F, Win) self.nenbw_disp = self.nenbw self.cgain_disp = self.cgain self.sidelobe_level_disp = self.sidelobe_level self.unit_nenbw = "bins" self.unit_scale = "" self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) window_name = self.win_dict['name'] param_txt = "" if self.win_dict['n_par'] > 0: param_txt = " (" + self.win_dict['par'][0][ 'name_tex'] + " = {0:.3g})".format( self.win_dict['par'][0]['val']) if self.win_dict['n_par'] > 1: param_txt = param_txt[:-1]\ + ", {0:s} = {1:.3g})".format(self.win_dict['par'][1]['name_tex'], self.win_dict['par'][1]['val']) self.mplwidget.fig.suptitle(r'{0} Window'.format(window_name) + param_txt) # plot a line at the max. sidelobe level if self.tbl_sel[3]: self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b') patch = mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0) # Info legend for time domain window labels_t = [] labels_t.append("$N$ = {0:d}".format(self.N)) self.ax_t.legend([patch], labels_t, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) # Info legend for frequency domain window labels_f = [] N_patches = 0 if self.tbl_sel[0]: labels_f.append("$NENBW$ = {0:.4g} {1}".format( self.nenbw_disp, self.unit_nenbw)) N_patches += 1 if self.tbl_sel[1]: labels_f.append("$CGAIN$ = {0:.4g} {1}".format( self.cgain_disp, self.unit_scale)) N_patches += 1 if self.tbl_sel[2]: labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f)) N_patches += 1 if N_patches > 0: self.ax_f.legend([patch] * N_patches, labels_f, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) np.seterr(**old_settings_seterr) self.update_info() self.redraw() #------------------------------------------------------------------------------ def update_info(self): """ Update the text info box for the window """ if 'info' in self.win_dict: self.txtInfoBox.setText(self.win_dict['info']) self._set_table_item(0, 0, "ENBW", font=self.bfont) #, sel=True) self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp)) self._set_table_item(0, 2, self.unit_nenbw) self._set_table_item(0, 3, "Scale", font=self.bfont) #, sel=True) self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp)) self._set_table_item(0, 5, self.unit_scale) self._set_table_item(1, 0, "1st Zero", font=self.bfont) #, sel=True) self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f)) self._set_table_item(1, 2, "f_S") self._set_table_item(1, 3, "Sidelobes", font=self.bfont) #, sel=True) self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp)) self._set_table_item(1, 5, self.unit_scale) self.tblWinProperties.resizeColumnsToContents() self.tblWinProperties.resizeRowsToContents() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw() self.needs_redraw = False
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.chk_auto_N = QCheckBox(self) self.chk_auto_N.setChecked(False) self.chk_auto_N.setToolTip( "Use number of points from calling routine.") self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N)) self.led_N.setMaximumWidth(70) self.led_N.setToolTip("<span>Number of window data points.</span>") self.chk_log_t = QCheckBox("Log", self) self.chk_log_t.setChecked(False) self.chk_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(50) self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel("dB", self) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.chk_norm_f = QCheckBox("Norm", self) self.chk_norm_f.setChecked(True) self.chk_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.chk_half_f = QCheckBox("Half", self) self.chk_half_f.setChecked(True) self.chk_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") self.chk_log_f = QCheckBox("Log", self) self.chk_log_f.setChecked(True) self.chk_log_f.setToolTip("Display in dB") self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(50) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_f = QLabel("dB", self) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) layHControls = QHBoxLayout() layHControls.addWidget(self.chk_auto_N) layHControls.addWidget(self.lbl_auto_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addStretch(10) layHControls.addWidget(self.chk_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_half_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.lbl_log_bottom_f) self.tblWinProperties = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tblWinProperties.setAlternatingRowColors(True) self.tblWinProperties.verticalHeader().setVisible(False) self.tblWinProperties.horizontalHeader().setVisible(False) self._init_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget: Layout layVMainMpl (VBox) is defined with MplWidget, # additional widgets can be added (like self.frmControls) # The widget encompasses all other widgets. #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. #---------------------------------------------------------------------- layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tblWinProperties) layVInfo.addWidget(self.txtInfoBox) self.frmInfo = QFrame(self) self.frmInfo.setObjectName("frmInfo") self.frmInfo.setLayout(layVInfo) #---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(self.frmInfo) # 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, 1000]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) #---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chk_log_f.clicked.connect(self.update_view) self.chk_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.chk_auto_N.clicked.connect(self.calc_N) self.led_N.editingFinished.connect(self.draw) self.chk_norm_f.clicked.connect(self.draw) self.chk_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tblWinProperties.itemClicked.connect(self._handle_item_clicked)
class Plot_FFT_win(QDialog): """ Create a pop-up widget for displaying time and frequency view of an FFT window. Data is passed via the dictionary `win_dict` that is specified during construction. Available windows, parameters, tooltipps etc are provided by the widget `pyfda_fft_windows_lib.QFFTWinSelection` Parameters ---------- parent : class instance reference to parent win_dict : dict dictionary derived from `pyfda_fft_windows_lib.all_windows_dict` with valid and available windows and their current settings (if applicable) sym : bool Passed to `get_window()`: When True, generate a symmetric window for use in filter design. When False (default), generate a periodic window for use in spectral analysis. title : str Title text for Qt Window ignore_close_event : bool Disable close event when True (Default) Methods ------- - `self.calc_N()` - `self.update_view()`: - `self.draw()`: calculate window and FFT and draw both - `get_win(N)` : Get the window array """ sig_rx = pyqtSignal(object) # incoming sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent, win_dict, sym=False, title='pyFDA Window Viewer', ignore_close_event=True): super(Plot_FFT_win, self).__init__(parent) # make window stay on top qwindow_stay_on_top(self, True) self.win_dict = win_dict self.sym = sym self.ignore_close_event = ignore_close_event self.setWindowTitle(title) self.needs_calc = True self.bottom_f = -80 # min. value for dB display self.bottom_t = -60 # initial number of data points for visualization self.N_view = 32 self.pad = 16 # zero padding factor for smooth FFT plot # initial settings for checkboxes self.tbl_sel = [True, True, False, False] # False, False, False, False] self.tbl_cols = 6 self.tbl_rows = len(self.tbl_sel) // (self.tbl_cols // 3) self._construct_UI() self.calc_win_draw() # ------------------------------------------------------------------------------ def closeEvent(self, event): """ Catch `closeEvent` (user has tried to close the FFT window) and send a signal to parent to decide how to proceed. This can be disabled by setting `self.ignore_close_event = False` e.g. for instantiating the widget as a standalone window. """ if self.ignore_close_event: event.ignore() self.emit({'closeEvent': ''}) # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx: - `self.calc_N` - `self.update_view`: - `self.draw`: calculate window and FFT and draw both """ # logger.debug("PROCESS_SIG_RX - vis={0}, needs_calc={1}\n{2}" # .format(self.isVisible(), self.needs_calc, pprint_log(dict_sig))) if dict_sig['id'] == id(self): logger.warning("Stopped infinite loop:\n{0}".format( pprint_log(dict_sig))) return elif not self.isVisible(): self.needs_calc = True elif 'view_changed' in dict_sig and 'fft_win' in dict_sig['view_changed']\ or self.needs_calc: self.calc_win_draw() self.needs_calc = False elif 'home' in dict_sig: self.update_view() else: logger.error("Unknown content of dict_sig: {0}".format(dict_sig)) # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.qfft_win_select = QFFTWinSelector(self, self.win_dict) self.lbl_N = QLabel(to_html("N =", frmt='bi')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N_view)) self.led_N.setMaximumWidth(qtext_width(N_x=8)) self.led_N.setToolTip( "<span>Number of window data points to display.</span>") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_t = QPushButton("dB", default=False, autoDefault=False) self.but_log_t.setMaximumWidth(qtext_width(" dB ")) self.but_log_t.setObjectName("chk_log_time") self.but_log_t.setCheckable(True) self.but_log_t.setChecked(False) self.but_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.but_norm_f = QPushButton("Max=1", default=False, autoDefault=False) self.but_norm_f.setCheckable(True) self.but_norm_f.setChecked(True) self.but_norm_f.setMaximumWidth(qtext_width(text=" Max=1 ")) self.but_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.but_half_f = QPushButton("0...½", default=False, autoDefault=False) self.but_half_f.setCheckable(True) self.but_half_f.setChecked(True) self.but_half_f.setMaximumWidth(qtext_width(text=" 0...½ ")) self.but_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_f = QPushButton("dB", default=False, autoDefault=False) self.but_log_f.setMaximumWidth(qtext_width(" dB ")) self.but_log_f.setObjectName("chk_log_freq") self.but_log_f.setToolTip("<span>Display in dB.</span>") self.but_log_f.setCheckable(True) self.but_log_f.setChecked(True) self.lbl_log_bottom_f = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked()) self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") # ---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets # ---------------------------------------------------------------------- layH_win_select = QHBoxLayout() layH_win_select.addWidget(self.qfft_win_select) layH_win_select.setContentsMargins(0, 0, 0, 0) layH_win_select.addStretch(1) frmQFFT = QFrame(self) frmQFFT.setObjectName("frmQFFT") frmQFFT.setLayout(layH_win_select) hline = QHLine() layHControls = QHBoxLayout() layHControls.addWidget(self.lbl_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.but_log_t) layHControls.addStretch(5) layHControls.addWidget(QVLine(width=2)) layHControls.addStretch(5) layHControls.addWidget(self.but_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.but_half_f) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.but_log_f) layVControls = QVBoxLayout() layVControls.addWidget(frmQFFT) layVControls.addWidget(hline) layVControls.addLayout(layHControls) frmControls = QFrame(self) frmControls.setObjectName("frmControls") frmControls.setLayout(layVControls) # ---------------------------------------------------------------------- # ### mplwidget ### # # Layout layVMainMpl (VBox) is defined within MplWidget, additional # widgets can be added below the matplotlib widget (here: frmControls) # # ---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(frmControls) self.mplwidget.layVMainMpl.setContentsMargins(0, 0, 0, 0) # ---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. # ---------------------------------------------------------------------- self.tbl_win_props = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tbl_win_props.setAlternatingRowColors(True) # Auto-resize of table can be set using the header (although it is invisible) self.tbl_win_props.verticalHeader().setSectionResizeMode( QHeaderView.Stretch) # Only the columns with data are stretched, the others are minimum size self.tbl_win_props.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) self.tbl_win_props.horizontalHeader().setSectionResizeMode( 4, QHeaderView.Stretch) self.tbl_win_props.verticalHeader().setVisible(False) self.tbl_win_props.horizontalHeader().setVisible(False) self.tbl_win_props.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.tbl_win_props.setFixedHeight( self.tbl_win_props.rowHeight(0) * self.tbl_rows + self.tbl_win_props.frameWidth() * 2) # self.tbl_win_props.setVerticalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) # self.tbl_win_props.setHorizontalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) self._construct_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tbl_win_props) layVInfo.addWidget(self.txtInfoBox) frmInfo = QFrame(self) frmInfo.setObjectName("frmInfo") frmInfo.setLayout(layVInfo) # ---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all subwidgets # ---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(frmInfo) # 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, 800]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) # ---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.calc_win_draw() # initial calculation and drawing # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) self.sig_rx.connect(self.qfft_win_select.sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_log_f.clicked.connect(self.update_view) self.but_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.led_N.editingFinished.connect(self.calc_win_draw) self.but_norm_f.clicked.connect(self.calc_win_draw) self.but_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tbl_win_props.itemClicked.connect(self._handle_item_clicked) self.qfft_win_select.sig_tx.connect(self.update_fft_win) # ------------------------------------------------------------------------------ def _construct_table(self, rows, cols, val): """ Create a table with `rows` and `cols`, organized in sets of 3: Name (with a checkbox) - value - unit each item. Parameters ---------- rows : int number of rows cols : int number of columns (must be multiple of 3) val : str initialization value for the table Returns ------- None """ for r in range(rows): for c in range(cols): item = QTableWidgetItem(val) if c % 3 == 0: item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if self.tbl_sel[r * 2 + c % 3]: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self.tbl_win_props.setItem(r, c, item) # https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget # ------------------------------------------------------------------------------ def update_fft_win(self, dict_sig=None): """ Update FFT window when window or parameters have changed and pass thru 'view_changed':'fft_win_type' or 'fft_win_par' """ self.calc_win_draw() self.emit(dict_sig) # ------------------------------------------------------------------------------ def calc_win_draw(self): """ (Re-)Calculate the window, its FFT and some characteristic values and update the plot of the window and its FFT. This should be triggered when the window type or length or a parameters has been changed. Returns ------- None Attributes ---------- """ self.N_view = safe_eval(self.led_N.text(), self.N_view, sign='pos', return_type='int') self.led_N.setText(str(self.N_view)) self.n = np.arange(self.N_view) self.win_view = self.qfft_win_select.get_window(self.N_view, sym=self.sym) if self.qfft_win_select.err: self.qfft_win_select.dict2ui() self.nenbw = self.N_view * np.sum(np.square(self.win_view))\ / np.square(np.sum(self.win_view)) self.cgain = np.sum(self.win_view) / self.N_view # coherent gain # calculate the FFT of the window with a zero padding factor # of `self.pad` self.F = fftfreq(self.N_view * self.pad, d=1. / fb.fil[0]['f_S']) self.Win = np.abs(fft(self.win_view, self.N_view * self.pad)) # Correct gain for periodic signals (coherent gain) if self.but_norm_f.isChecked(): self.Win /= (self.N_view * self.cgain) # calculate frequency of first zero and maximum sidelobe level first_zero = argrelextrema(self.Win[:(self.N_view * self.pad) // 2], np.less) if np.shape(first_zero)[1] > 0: first_zero = first_zero[0][0] self.first_zero_f = self.F[first_zero] self.sidelobe_level = np.max( self.Win[first_zero:(self.N_view * self.pad) // 2]) else: self.first_zero_f = np.nan self.sidelobe_level = 0 self.update_view() # ------------------------------------------------------------------------------ def _set_table_item(self, row, col, val, font=None, sel=None): """ Set the table item with the index `row, col` and the value val """ item = self.tbl_win_props.item(row, col) item.setText(str(val)) if font: self.tbl_win_props.item(row, col).setFont(font) if sel is True: item.setCheckState(Qt.Checked) if sel is False: item.setCheckState(Qt.Unchecked) # when sel is not specified, don't change anything # ------------------------------------------------------------------------------ def _handle_item_clicked(self, item): if item.column() % 3 == 0: # clicked on checkbox num = item.row() * 2 + item.column() // 3 if item.checkState() == Qt.Checked: self.tbl_sel[num] = True logger.debug('"{0}:{1}" Checked'.format(item.text(), num)) else: self.tbl_sel[num] = False logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num)) elif item.column() % 3 == 1: # clicked on value field logger.info("{0:s} copied to clipboard.".format(item.text())) fb.clipboard.setText(item.text()) self.update_view() # ------------------------------------------------------------------------------ def update_bottom(self): """ Update log bottom settings """ self.bottom_t = safe_eval(self.led_log_bottom_t.text(), self.bottom_t, sign='neg', return_type='float') self.led_log_bottom_t.setText(str(self.bottom_t)) self.bottom_f = safe_eval(self.led_log_bottom_f.text(), self.bottom_f, sign='neg', return_type='float') self.led_log_bottom_f.setText(str(self.bottom_f)) self.update_view() # ------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale, lin/log etc without recalculating the window or its FFT. """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') self.ax_t.cla() self.ax_f.cla() self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$') self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$') if self.but_log_t.isChecked(): self.ax_t.plot( self.n, np.maximum(20 * np.log10(np.abs(self.win_view)), self.bottom_t)) else: self.ax_t.plot(self.n, self.win_view) if self.but_half_f.isChecked(): F = self.F[:len(self.F * self.pad) // 2] Win = self.Win[:len(self.F * self.pad) // 2] else: F = fftshift(self.F) Win = fftshift(self.Win) if self.but_log_f.isChecked(): self.ax_f.plot( F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f)) self.nenbw_disp = 10 * np.log10(self.nenbw) self.cgain_disp = 20 * np.log10(self.cgain) self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level) self.nenbw_unit = "dB" self.cgain_unit = "dB" else: self.ax_f.plot(F, Win) self.nenbw_disp = self.nenbw self.cgain_disp = self.cgain self.sidelobe_level_disp = self.sidelobe_level self.nenbw_unit = "bins" self.cgain_unit = "" self.led_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_f.setVisible(self.but_log_f.isChecked()) self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked()) cur = self.win_dict['cur_win_name'] cur_win_d = self.win_dict[cur] param_txt = "" if cur_win_d['n_par'] > 0: if type(cur_win_d['par'][0]['val']) in {str}: p1 = cur_win_d['par'][0]['val'] else: p1 = "{0:.3g}".format(cur_win_d['par'][0]['val']) param_txt = " ({0:s} = {1:s})".format( cur_win_d['par'][0]['name_tex'], p1) if self.win_dict[cur]['n_par'] > 1: if type(cur_win_d['par'][1]['val']) in {str}: p2 = cur_win_d['par'][1]['val'] else: p2 = "{0:.3g}".format(cur_win_d['par'][1]['val']) param_txt = param_txt[:-1]\ + ", {0:s} = {1:s})".format(cur_win_d['par'][1]['name_tex'], p2) self.mplwidget.fig.suptitle(r'{0} Window'.format(cur) + param_txt) # plot a line at the max. sidelobe level if self.tbl_sel[3]: self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b') patch = mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0) # Info legend for time domain window labels_t = [] labels_t.append("$N$ = {0:d}".format(self.N_view)) self.ax_t.legend([patch], labels_t, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) # Info legend for frequency domain window labels_f = [] N_patches = 0 if self.tbl_sel[0]: labels_f.append("$NENBW$ = {0:.4g} {1}".format( self.nenbw_disp, self.nenbw_unit)) N_patches += 1 if self.tbl_sel[1]: labels_f.append("$CGAIN$ = {0:.4g} {1}".format( self.cgain_disp, self.cgain_unit)) N_patches += 1 if self.tbl_sel[2]: labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f)) N_patches += 1 if N_patches > 0: self.ax_f.legend([patch] * N_patches, labels_f, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) np.seterr(**old_settings_seterr) self.update_info() self.redraw() # ------------------------------------------------------------------------------ def update_info(self): """ Update the text info box for the window """ cur = self.win_dict['cur_win_name'] if 'info' in self.win_dict[cur]: self.txtInfoBox.setText(self.win_dict[cur]['info']) else: self.txtInfoBox.clear() self._set_table_item(0, 0, "NENBW", font=self.bfont) # , sel=True) self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp)) self._set_table_item(0, 2, self.nenbw_unit) self._set_table_item(0, 3, "Scale", font=self.bfont) # , sel=True) self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp)) self._set_table_item(0, 5, self.cgain_unit) self._set_table_item(1, 0, "1st Zero", font=self.bfont) # , sel=True) self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f)) self._set_table_item(1, 2, "f_S") self._set_table_item(1, 3, "Sidelobes", font=self.bfont) # , sel=True) self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp)) self._set_table_item(1, 5, self.cgain_unit) self.tbl_win_props.resizeColumnsToContents() self.tbl_win_props.resizeRowsToContents() # ----------------------------------------------------------------------------- def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.qfft_win_select = QFFTWinSelector(self, self.win_dict) self.lbl_N = QLabel(to_html("N =", frmt='bi')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N_view)) self.led_N.setMaximumWidth(qtext_width(N_x=8)) self.led_N.setToolTip( "<span>Number of window data points to display.</span>") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_t = QPushButton("dB", default=False, autoDefault=False) self.but_log_t.setMaximumWidth(qtext_width(" dB ")) self.but_log_t.setObjectName("chk_log_time") self.but_log_t.setCheckable(True) self.but_log_t.setChecked(False) self.but_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.but_norm_f = QPushButton("Max=1", default=False, autoDefault=False) self.but_norm_f.setCheckable(True) self.but_norm_f.setChecked(True) self.but_norm_f.setMaximumWidth(qtext_width(text=" Max=1 ")) self.but_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.but_half_f = QPushButton("0...½", default=False, autoDefault=False) self.but_half_f.setCheckable(True) self.but_half_f.setChecked(True) self.but_half_f.setMaximumWidth(qtext_width(text=" 0...½ ")) self.but_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_f = QPushButton("dB", default=False, autoDefault=False) self.but_log_f.setMaximumWidth(qtext_width(" dB ")) self.but_log_f.setObjectName("chk_log_freq") self.but_log_f.setToolTip("<span>Display in dB.</span>") self.but_log_f.setCheckable(True) self.but_log_f.setChecked(True) self.lbl_log_bottom_f = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked()) self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") # ---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets # ---------------------------------------------------------------------- layH_win_select = QHBoxLayout() layH_win_select.addWidget(self.qfft_win_select) layH_win_select.setContentsMargins(0, 0, 0, 0) layH_win_select.addStretch(1) frmQFFT = QFrame(self) frmQFFT.setObjectName("frmQFFT") frmQFFT.setLayout(layH_win_select) hline = QHLine() layHControls = QHBoxLayout() layHControls.addWidget(self.lbl_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.but_log_t) layHControls.addStretch(5) layHControls.addWidget(QVLine(width=2)) layHControls.addStretch(5) layHControls.addWidget(self.but_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.but_half_f) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.but_log_f) layVControls = QVBoxLayout() layVControls.addWidget(frmQFFT) layVControls.addWidget(hline) layVControls.addLayout(layHControls) frmControls = QFrame(self) frmControls.setObjectName("frmControls") frmControls.setLayout(layVControls) # ---------------------------------------------------------------------- # ### mplwidget ### # # Layout layVMainMpl (VBox) is defined within MplWidget, additional # widgets can be added below the matplotlib widget (here: frmControls) # # ---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(frmControls) self.mplwidget.layVMainMpl.setContentsMargins(0, 0, 0, 0) # ---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. # ---------------------------------------------------------------------- self.tbl_win_props = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tbl_win_props.setAlternatingRowColors(True) # Auto-resize of table can be set using the header (although it is invisible) self.tbl_win_props.verticalHeader().setSectionResizeMode( QHeaderView.Stretch) # Only the columns with data are stretched, the others are minimum size self.tbl_win_props.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) self.tbl_win_props.horizontalHeader().setSectionResizeMode( 4, QHeaderView.Stretch) self.tbl_win_props.verticalHeader().setVisible(False) self.tbl_win_props.horizontalHeader().setVisible(False) self.tbl_win_props.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.tbl_win_props.setFixedHeight( self.tbl_win_props.rowHeight(0) * self.tbl_rows + self.tbl_win_props.frameWidth() * 2) # self.tbl_win_props.setVerticalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) # self.tbl_win_props.setHorizontalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) self._construct_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tbl_win_props) layVInfo.addWidget(self.txtInfoBox) frmInfo = QFrame(self) frmInfo.setObjectName("frmInfo") frmInfo.setLayout(layVInfo) # ---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all subwidgets # ---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(frmInfo) # 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, 800]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) # ---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.calc_win_draw() # initial calculation and drawing # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) self.sig_rx.connect(self.qfft_win_select.sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_log_f.clicked.connect(self.update_view) self.but_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.led_N.editingFinished.connect(self.calc_win_draw) self.but_norm_f.clicked.connect(self.calc_win_draw) self.but_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tbl_win_props.itemClicked.connect(self._handle_item_clicked) self.qfft_win_select.sig_tx.connect(self.update_fft_win)
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.chkFiltPerf = QCheckBox("H(f)", self) self.chkFiltPerf.setChecked(True) self.chkFiltPerf.setToolTip( "Display frequency response at test frequencies.") self.chkDocstring = QCheckBox("Doc$", self) self.chkDocstring.setChecked(False) self.chkDocstring.setToolTip( "Display docstring from python filter method.") self.chkRichText = QCheckBox("RTF", self) self.chkRichText.setChecked(HAS_DOCUTILS) self.chkRichText.setEnabled(HAS_DOCUTILS) self.chkRichText.setToolTip( "Render documentation in Rich Text Format.") self.chkFiltDict = QCheckBox("FiltDict", self) self.chkFiltDict.setToolTip("Show filter dictionary for debugging.") self.chkFiltTree = QCheckBox("FiltTree", self) self.chkFiltTree.setToolTip("Show filter tree for debugging.") self.layHChkBoxes = QHBoxLayout() self.layHChkBoxes.addWidget(self.chkFiltPerf) self.layHChkBoxes.addStretch(1) self.layHChkBoxes.addWidget(self.chkDocstring) self.layHChkBoxes.addStretch(1) self.layHChkBoxes.addWidget(self.chkRichText) self.layHChkBoxes.addStretch(1) self.layHChkBoxes.addWidget(self.chkFiltDict) self.layHChkBoxes.addStretch(1) self.layHChkBoxes.addWidget(self.chkFiltTree) self.frmMain = QFrame(self) self.frmMain.setLayout(self.layHChkBoxes) 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.layHChkBoxes) 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.chkFiltPerf.clicked.connect(self._show_filt_perf) self.chkFiltDict.clicked.connect(self._show_filt_dict) self.chkFiltTree.clicked.connect(self._show_filt_tree) self.chkDocstring.clicked.connect(self._show_doc) self.chkRichText.clicked.connect(self._show_doc)