Exemplo n.º 1
0
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)
Exemplo n.º 2
0
class PlotImpz_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming: not implemented at the moment, update_N is triggered directly
    # by plot_impz
    # sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)
    # outgoing: to fft related widgets (FFT window widget, qfft_win_select)
    sig_tx_fft = pyqtSignal(object)

    from pyfda.libs.pyfda_qt_lib import emit

    # ------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from
        - FFT window widget
        - qfft_win_select
        """

        # logger.debug("PROCESS_SIG_RX - vis: {0}\n{1}"
        #              .format(self.isVisible(), pprint_log(dict_sig)))

        if 'id' in dict_sig and dict_sig['id'] == id(self):
            logger.warning("Stopped infinite loop:\n{0}".format(
                pprint_log(dict_sig)))
            return

        # --- signals coming from the FFT window widget or the FFT window selector
        if dict_sig['class'] in {'Plot_FFT_win', 'QFFTWinSelector'}:
            if 'closeEvent' in dict_sig:  # hide FFT window widget and return
                self.hide_fft_wdg()
                return
            else:
                # check for value 'fft_win*':
                if 'view_changed' in dict_sig and 'fft_win' in dict_sig[
                        'view_changed']:
                    # local connection to FFT window widget and qfft_win_select
                    self.emit(dict_sig, sig_name='sig_tx_fft')
                    # global connection to e.g. plot_impz
                    self.emit(dict_sig)

# ------------------------------------------------------------------------------

    def __init__(self):
        super().__init__()
        """
        Intitialize the widget, consisting of:
        - top chkbox row
        - coefficient table
        - two bottom rows with action buttons
        """
        # initial settings
        self.N_start = 0
        self.N_user = 0
        self.N = 0
        self.N_frame_user = 0
        self.N_frame = 0

        # time
        self.plt_time_resp = "stem"
        self.plt_time_stim = "line"
        self.plt_time_stmq = "none"
        self.plt_time_spgr = "none"

        self.bottom_t = -80  # initial value for log. scale (time)
        self.time_nfft_spgr = 256  # number of fft points per spectrogram segment
        self.time_ovlp_spgr = 128  # number of overlap points between spectrogram segments
        self.mode_spgr_time = "psd"

        # frequency
        self.cmb_freq_display_item = "mag"
        self.plt_freq_resp = "line"
        self.plt_freq_stim = "none"
        self.plt_freq_stmq = "none"

        self.bottom_f = -120  # initial value for log. scale
        self.param = None

        self.f_scale = fb.fil[0]['f_S']
        self.t_scale = fb.fil[0]['T_S']
        # list of windows that are available for FFT analysis
        win_names_list = [
            "Boxcar", "Rectangular", "Barthann", "Bartlett", "Blackman",
            "Blackmanharris", "Bohman", "Cosine", "Dolph-Chebyshev", "Flattop",
            "General Gaussian", "Gauss", "Hamming", "Hann", "Kaiser",
            "Nuttall", "Parzen", "Slepian", "Triangular", "Tukey"
        ]
        self.cur_win_name = "Rectangular"  # set initial window type

        # initialize windows dict with the list above
        self.win_dict = get_windows_dict(win_names_list=win_names_list,
                                         cur_win_name=self.cur_win_name)

        # instantiate FFT window with default windows dict
        self.fft_widget = Plot_FFT_win(self,
                                       self.win_dict,
                                       sym=False,
                                       title="pyFDA Spectral Window Viewer")
        # hide window initially, this is modeless i.e. a non-blocking popup window
        self.fft_widget.hide()

        # data / icon / tooltipp (none) for plotting styles
        self.plot_styles_list = [
            ("Plot style"), ("none", QIcon(":/plot_style-none"), "off"),
            ("dots*", QIcon(":/plot_style-mkr"), "markers only"),
            ("line", QIcon(":/plot_style-line"), "line"),
            ("line*", QIcon(":/plot_style-line-mkr"), "line + markers"),
            ("stem", QIcon(":/plot_style-stem"), "stems"),
            ("stem*", QIcon(":/plot_style-stem-mkr"), "stems + markers"),
            ("steps", QIcon(":/plot_style-steps"), "steps"),
            ("steps*", QIcon(":/plot_style-steps-mkr"), "steps + markers")
        ]

        self.cmb_time_spgr_items = [
            "<span>Show Spectrogram for selected signal.</span>",
            ("none", "None", ""), ("xn", "x[n]", "input"),
            ("xqn", "x_q[n]", "quantized input"), ("yn", "y[n]", "output")
        ]

        self.cmb_mode_spgr_time_items = [
            "<span>Spectrogram display mode.</span>",
            ("psd", "PSD",
             "<span>Power Spectral Density, either per bin or referred to "
             "<i>f<sub>S</sub></i></span>"),
            ("magnitude", "Mag.", "Signal magnitude"),
            ("angle", "Angle", "Phase, wrapped to &pm; &pi;"),
            ("phase", "Phase", "Phase (unwrapped)")
        ]
        #        self.N

        self.cmb_freq_display_items = [
            "<span>Select how to display the spectrum.</span>",
            ("mag", "Magnitude", "<span>Spectral magnitude</span>"),
            ("mag_phi", "Mag. / Phase", "<span>Magnitude and phase.</span>"),
            ("re_im", "Re. / Imag.",
             "<span>Real and imaginary part of spectrum.</span>")
        ]

        self._construct_UI()
        #        self._enable_stim_widgets()
        self.update_N(emit=False)  # also updates window function and win_dict
#        self._update_noi()

    def _construct_UI(self):
        # ----------- ---------------------------------------------------
        # Run control widgets
        # ---------------------------------------------------------------
        # self.but_auto_run = QPushButtonRT(text=to_html("Auto", frmt="b"), margin=0)
        self.but_auto_run = QPushButton(" Auto", self)
        self.but_auto_run.setObjectName("but_auto_run")
        self.but_auto_run.setToolTip(
            "<span>Update response automatically when "
            "parameters have been changed.</span>")
        # self.but_auto_run.setMaximumWidth(qtext_width(text=" Auto "))
        self.but_auto_run.setCheckable(True)
        self.but_auto_run.setChecked(True)

        but_height = self.but_auto_run.sizeHint().height()

        self.but_run = QPushButton(self)
        self.but_run.setIcon(QIcon(":/play.svg"))

        self.but_run.setIconSize(QSize(but_height, but_height))
        self.but_run.setFixedSize(QSize(2 * but_height, but_height))
        self.but_run.setToolTip("Run simulation")
        self.but_run.setEnabled(True)

        self.cmb_sim_select = QComboBox(self)
        self.cmb_sim_select.addItems(["Float", "Fixpoint"])
        qset_cmb_box(self.cmb_sim_select, "Float")
        self.cmb_sim_select.setToolTip("<span>Simulate floating-point or "
                                       "fixpoint response.</span>")

        self.lbl_N_points = QLabel(to_html("N", frmt='bi') + " =", self)
        self.led_N_points = QLineEdit(self)
        self.led_N_points.setText(str(self.N))
        self.led_N_points.setToolTip(
            "<span>Last data point. "
            "<i>N</i> = 0 tries to choose for you.</span>")
        self.led_N_points.setMaximumWidth(qtext_width(N_x=8))
        self.lbl_N_start = QLabel(to_html("N_0", frmt='bi') + " =", self)
        self.led_N_start = QLineEdit(self)
        self.led_N_start.setText(str(self.N_start))
        self.led_N_start.setToolTip("<span>First point to plot.</span>")
        self.led_N_start.setMaximumWidth(qtext_width(N_x=8))

        self.lbl_N_frame = QLabel(to_html("&Delta;N", frmt='bi') + " =", self)
        self.led_N_frame = QLineEdit(self)
        self.led_N_frame.setText(str(self.N_frame))
        self.led_N_frame.setToolTip(
            "<span>Frame length; longer frames calculate faster but calculation cannot "
            "be stopped so quickly. "
            "<i>&Delta;N</i> = 0 calculates all samples in one frame.</span>")
        self.led_N_frame.setMaximumWidth(qtext_width(N_x=8))

        self.prg_wdg = QProgressBar(self)
        self.prg_wdg.setFixedHeight(but_height)
        self.prg_wdg.setFixedWidth(qtext_width(N_x=6))
        self.prg_wdg.setMinimum(0)
        self.prg_wdg.setValue(0)

        self.but_toggle_stim_options = PushButton(" Stimuli ", checked=True)
        self.but_toggle_stim_options.setObjectName("but_stim_options")
        self.but_toggle_stim_options.setToolTip(
            "<span>Show / hide stimulus options.</span>")

        self.lbl_stim_cmplx_warn = QLabel(self)
        self.lbl_stim_cmplx_warn = QLabel(to_html("Cmplx!", frmt='b'), self)
        self.lbl_stim_cmplx_warn.setToolTip(
            '<span>Signal is complex valued; '
            'single-sided and H<sub>id</sub> spectra may be wrong.</span>')
        self.lbl_stim_cmplx_warn.setStyleSheet("background-color : yellow;"
                                               "border : 1px solid grey")

        self.but_fft_wdg = QPushButton(self)
        self.but_fft_wdg.setIcon(QIcon(":/fft.svg"))
        self.but_fft_wdg.setIconSize(QSize(but_height, but_height))
        self.but_fft_wdg.setFixedSize(QSize(int(1.5 * but_height), but_height))
        self.but_fft_wdg.setToolTip(
            '<span>Show / hide FFT widget (select window type '
            ' and display its properties).</span>')
        self.but_fft_wdg.setCheckable(True)
        self.but_fft_wdg.setChecked(False)

        self.qfft_win_select = QFFTWinSelector(self, self.win_dict)

        self.but_fx_scale = PushButton(" FX:Int ")
        self.but_fx_scale.setObjectName("but_fx_scale")
        self.but_fx_scale.setToolTip(
            "<span>Display data with integer (fixpoint) scale.</span>")

        self.but_fx_range = PushButton(" FX:Range")
        self.but_fx_range.setObjectName("but_fx_limits")
        self.but_fx_range.setToolTip(
            "<span>Display limits of fixpoint range.</span>")

        layH_ctrl_run = QHBoxLayout()
        layH_ctrl_run.addWidget(self.but_auto_run)
        layH_ctrl_run.addWidget(self.but_run)
        layH_ctrl_run.addWidget(self.cmb_sim_select)
        layH_ctrl_run.addSpacing(10)
        layH_ctrl_run.addWidget(self.lbl_N_start)
        layH_ctrl_run.addWidget(self.led_N_start)
        layH_ctrl_run.addWidget(self.lbl_N_points)
        layH_ctrl_run.addWidget(self.led_N_points)
        layH_ctrl_run.addWidget(self.lbl_N_frame)
        layH_ctrl_run.addWidget(self.led_N_frame)
        layH_ctrl_run.addWidget(self.prg_wdg)

        layH_ctrl_run.addSpacing(20)
        layH_ctrl_run.addWidget(self.but_toggle_stim_options)
        layH_ctrl_run.addSpacing(5)
        layH_ctrl_run.addWidget(self.lbl_stim_cmplx_warn)
        layH_ctrl_run.addSpacing(20)
        layH_ctrl_run.addWidget(self.but_fft_wdg)
        layH_ctrl_run.addWidget(self.qfft_win_select)
        layH_ctrl_run.addSpacing(20)
        layH_ctrl_run.addWidget(self.but_fx_scale)
        layH_ctrl_run.addWidget(self.but_fx_range)
        layH_ctrl_run.addStretch(10)

        # layH_ctrl_run.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_run = QWidget(self)
        self.wdg_ctrl_run.setLayout(layH_ctrl_run)
        # --- end of run control ----------------------------------------

        # ----------- ---------------------------------------------------
        # Controls for time domain
        # ---------------------------------------------------------------
        self.lbl_plt_time_stim = QLabel(to_html("Stim. x", frmt='bi'), self)
        self.cmb_plt_time_stim = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_time_stim, self.plot_styles_list,
                          self.plt_time_stim)
        self.cmb_plt_time_stim.setToolTip(
            "<span>Plot style for stimulus.</span>")

        self.lbl_plt_time_stmq = QLabel(
            to_html("&nbsp;&nbsp;Fixp. Stim. x_Q", frmt='bi'), self)
        self.cmb_plt_time_stmq = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_time_stmq, self.plot_styles_list,
                          self.plt_time_stmq)
        self.cmb_plt_time_stmq.setToolTip(
            "<span>Plot style for <em>fixpoint</em> "
            "(quantized) stimulus.</span>")

        lbl_plt_time_resp = QLabel(to_html("&nbsp;&nbsp;Resp. y", frmt='bi'),
                                   self)
        self.cmb_plt_time_resp = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_time_resp, self.plot_styles_list,
                          self.plt_time_resp)
        self.cmb_plt_time_resp.setToolTip(
            "<span>Plot style for response.</span>")

        self.lbl_win_time = QLabel(to_html("&nbsp;&nbsp;Win", frmt='bi'), self)
        self.chk_win_time = QCheckBox(self)
        self.chk_win_time.setObjectName("chk_win_time")
        self.chk_win_time.setToolTip(
            '<span>Plot FFT windowing function.</span>')
        self.chk_win_time.setChecked(False)

        line1 = QVLine()
        line2 = QVLine(width=5)

        self.but_log_time = PushButton(" dB")
        self.but_log_time.setObjectName("but_log_time")
        self.but_log_time.setToolTip(
            "<span>Logarithmic scale for y-axis.</span>")

        lbl_plt_time_spgr = QLabel(to_html("Spectrogram", frmt='bi'), self)
        self.cmb_plt_time_spgr = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_time_spgr, self.cmb_time_spgr_items,
                          self.plt_time_spgr)
        spgr_en = self.plt_time_spgr != "none"

        self.cmb_mode_spgr_time = QComboBox(self)
        qcmb_box_populate(self.cmb_mode_spgr_time,
                          self.cmb_mode_spgr_time_items, self.mode_spgr_time)
        self.cmb_mode_spgr_time.setVisible(spgr_en)

        self.lbl_byfs_spgr_time = QLabel(to_html("&nbsp;per f_S", frmt='b'),
                                         self)
        self.lbl_byfs_spgr_time.setVisible(spgr_en)
        self.chk_byfs_spgr_time = QCheckBox(self)
        self.chk_byfs_spgr_time.setObjectName("chk_log_spgr")
        self.chk_byfs_spgr_time.setToolTip("<span>Display spectral density "
                                           "i.e. scale by f_S</span>")
        self.chk_byfs_spgr_time.setChecked(True)
        self.chk_byfs_spgr_time.setVisible(spgr_en)

        self.but_log_spgr_time = QPushButton("dB")
        self.but_log_spgr_time.setMaximumWidth(qtext_width(text=" dB"))
        self.but_log_spgr_time.setObjectName("but_log_spgr")
        self.but_log_spgr_time.setToolTip(
            "<span>Logarithmic scale for spectrogram.</span>")
        self.but_log_spgr_time.setCheckable(True)
        self.but_log_spgr_time.setChecked(True)
        self.but_log_spgr_time.setVisible(spgr_en)

        self.lbl_time_nfft_spgr = QLabel(to_html("&nbsp;N_FFT =", frmt='bi'),
                                         self)
        self.lbl_time_nfft_spgr.setVisible(spgr_en)
        self.led_time_nfft_spgr = QLineEdit(self)
        self.led_time_nfft_spgr.setText(str(self.time_nfft_spgr))
        self.led_time_nfft_spgr.setToolTip("<span>Number of FFT points per "
                                           "spectrogram segment.</span>")
        self.led_time_nfft_spgr.setVisible(spgr_en)

        self.lbl_time_ovlp_spgr = QLabel(to_html("&nbsp;N_OVLP =", frmt='bi'),
                                         self)
        self.lbl_time_ovlp_spgr.setVisible(spgr_en)
        self.led_time_ovlp_spgr = QLineEdit(self)
        self.led_time_ovlp_spgr.setText(str(self.time_ovlp_spgr))
        self.led_time_ovlp_spgr.setToolTip(
            "<span>Number of overlap data points "
            "between spectrogram segments.</span>")
        self.led_time_ovlp_spgr.setVisible(spgr_en)

        self.lbl_log_bottom_time = QLabel(to_html("min =", frmt='bi'), self)
        self.led_log_bottom_time = QLineEdit(self)
        self.led_log_bottom_time.setText(str(self.bottom_t))
        self.led_log_bottom_time.setMaximumWidth(qtext_width(N_x=8))
        self.led_log_bottom_time.setToolTip(
            "<span>Minimum display value for time and spectrogram plots with log. scale."
            "</span>")
        self.lbl_log_bottom_time.setVisible(
            self.but_log_time.isChecked()
            or (spgr_en and self.but_log_spgr_time.isChecked()))
        self.led_log_bottom_time.setVisible(
            self.lbl_log_bottom_time.isVisible())

        # self.lbl_colorbar_time = QLabel(to_html("&nbsp;Col.bar", frmt='b'), self)
        # self.lbl_colorbar_time.setVisible(spgr_en)
        # self.chk_colorbar_time = QCheckBox(self)
        # self.chk_colorbar_time.setObjectName("chk_colorbar_time")
        # self.chk_colorbar_time.setToolTip("<span>Enable colorbar</span>")
        # self.chk_colorbar_time.setChecked(True)
        # self.chk_colorbar_time.setVisible(spgr_en)

        layH_ctrl_time = QHBoxLayout()
        layH_ctrl_time.addWidget(self.lbl_plt_time_stim)
        layH_ctrl_time.addWidget(self.cmb_plt_time_stim)
        #
        layH_ctrl_time.addWidget(self.lbl_plt_time_stmq)
        layH_ctrl_time.addWidget(self.cmb_plt_time_stmq)
        #
        layH_ctrl_time.addWidget(lbl_plt_time_resp)
        layH_ctrl_time.addWidget(self.cmb_plt_time_resp)
        #
        layH_ctrl_time.addWidget(self.lbl_win_time)
        layH_ctrl_time.addWidget(self.chk_win_time)
        layH_ctrl_time.addSpacing(5)
        layH_ctrl_time.addWidget(line1)
        layH_ctrl_time.addSpacing(5)
        #
        layH_ctrl_time.addWidget(self.lbl_log_bottom_time)
        layH_ctrl_time.addWidget(self.led_log_bottom_time)
        layH_ctrl_time.addWidget(self.but_log_time)

        layH_ctrl_time.addSpacing(5)
        layH_ctrl_time.addWidget(line2)
        layH_ctrl_time.addSpacing(5)
        #
        layH_ctrl_time.addWidget(lbl_plt_time_spgr)
        layH_ctrl_time.addWidget(self.cmb_plt_time_spgr)
        layH_ctrl_time.addWidget(self.cmb_mode_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_byfs_spgr_time)
        layH_ctrl_time.addWidget(self.chk_byfs_spgr_time)
        layH_ctrl_time.addWidget(self.but_log_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_time_nfft_spgr)
        layH_ctrl_time.addWidget(self.led_time_nfft_spgr)
        layH_ctrl_time.addWidget(self.lbl_time_ovlp_spgr)
        layH_ctrl_time.addWidget(self.led_time_ovlp_spgr)

        layH_ctrl_time.addStretch(10)

        # layH_ctrl_time.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_time = QWidget(self)
        self.wdg_ctrl_time.setLayout(layH_ctrl_time)
        # ---- end time domain ------------------

        # ---------------------------------------------------------------
        # Controls for frequency domain
        # ---------------------------------------------------------------
        self.lbl_plt_freq_stim = QLabel(to_html("Stimulus X", frmt='bi'), self)
        self.cmb_plt_freq_stim = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_freq_stim, self.plot_styles_list,
                          self.plt_freq_stim)
        self.cmb_plt_freq_stim.setToolTip(
            "<span>Plot style for stimulus.</span>")

        self.lbl_plt_freq_stmq = QLabel(
            to_html("&nbsp;Fixp. Stim. X_Q", frmt='bi'), self)
        self.cmb_plt_freq_stmq = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_freq_stmq, self.plot_styles_list,
                          self.plt_freq_stmq)
        self.cmb_plt_freq_stmq.setToolTip(
            "<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>"
        )

        lbl_plt_freq_resp = QLabel(to_html("&nbsp;Response Y", frmt='bi'),
                                   self)
        self.cmb_plt_freq_resp = QComboBox(self)
        qcmb_box_populate(self.cmb_plt_freq_resp, self.plot_styles_list,
                          self.plt_freq_resp)
        self.cmb_plt_freq_resp.setToolTip(
            "<span>Plot style for response.</span>")

        self.but_log_freq = QPushButton("dB")
        self.but_log_freq.setMaximumWidth(qtext_width(" dB"))
        self.but_log_freq.setObjectName(".but_log_freq")
        self.but_log_freq.setToolTip(
            "<span>Logarithmic scale for y-axis.</span>")
        self.but_log_freq.setCheckable(True)
        self.but_log_freq.setChecked(True)

        self.lbl_log_bottom_freq = QLabel(to_html("min =", frmt='bi'), self)
        self.lbl_log_bottom_freq.setVisible(self.but_log_freq.isChecked())
        self.led_log_bottom_freq = QLineEdit(self)
        self.led_log_bottom_freq.setText(str(self.bottom_f))
        self.led_log_bottom_freq.setMaximumWidth(qtext_width(N_x=8))
        self.led_log_bottom_freq.setToolTip(
            "<span>Minimum display value for log. scale.</span>")
        self.led_log_bottom_freq.setVisible(self.but_log_freq.isChecked())

        if not self.but_log_freq.isChecked():
            self.bottom_f = 0

        self.cmb_freq_display = QComboBox(self)
        qcmb_box_populate(self.cmb_freq_display, self.cmb_freq_display_items,
                          self.cmb_freq_display_item)
        self.cmb_freq_display.setObjectName("cmb_re_im_freq")

        self.but_Hf = QPushButtonRT(self, to_html("H_id", frmt="bi"), margin=5)
        self.but_Hf.setObjectName("chk_Hf")
        self.but_Hf.setToolTip(
            "<span>Show ideal frequency response, calculated "
            "from the filter coefficients.</span>")
        self.but_Hf.setChecked(False)
        self.but_Hf.setCheckable(True)

        self.but_freq_norm_impz = QPushButtonRT(
            text="<b><i>E<sub>X</sub></i> = 1</b>", margin=5)
        self.but_freq_norm_impz.setToolTip(
            "<span>Normalize the FFT of the stimulus with <i>N<sub>FFT</sub></i> for "
            "<i>E<sub>X</sub></i> = 1. For a dirac pulse, this yields "
            "|<i>Y(f)</i>| = |<i>H(f)</i>|. DC and Noise need to be "
            "turned off, window should be <b>Rectangular</b>.</span>")
        self.but_freq_norm_impz.setCheckable(True)
        self.but_freq_norm_impz.setChecked(True)
        self.but_freq_norm_impz.setObjectName("freq_norm_impz")

        self.but_freq_show_info = QPushButton("Info", self)
        self.but_freq_show_info.setMaximumWidth(qtext_width(" Info "))
        self.but_freq_show_info.setObjectName("but_show_info_freq")
        self.but_freq_show_info.setToolTip(
            "<span>Show signal power in legend.</span>")
        self.but_freq_show_info.setCheckable(True)
        self.but_freq_show_info.setChecked(False)

        layH_ctrl_freq = QHBoxLayout()
        layH_ctrl_freq.addWidget(self.lbl_plt_freq_stim)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_stim)
        #
        layH_ctrl_freq.addWidget(self.lbl_plt_freq_stmq)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_stmq)
        #
        layH_ctrl_freq.addWidget(lbl_plt_freq_resp)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_resp)
        #
        layH_ctrl_freq.addSpacing(5)
        layH_ctrl_freq.addWidget(self.but_Hf)
        layH_ctrl_freq.addStretch(1)
        #
        layH_ctrl_freq.addWidget(self.lbl_log_bottom_freq)
        layH_ctrl_freq.addWidget(self.led_log_bottom_freq)
        layH_ctrl_freq.addWidget(self.but_log_freq)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(self.cmb_freq_display)
        layH_ctrl_freq.addStretch(1)

        layH_ctrl_freq.addWidget(self.but_freq_norm_impz)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(self.but_freq_show_info)
        layH_ctrl_freq.addStretch(10)

        # layH_ctrl_freq.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_freq = QWidget(self)
        self.wdg_ctrl_freq.setLayout(layH_ctrl_freq)
        # ---- end Frequency Domain ------------------

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        # connect FFT widget to qfft_selector and vice versa and to and signals upstream:
        self.fft_widget.sig_tx.connect(self.process_sig_rx)
        self.qfft_win_select.sig_tx.connect(self.process_sig_rx)
        # connect process_sig_rx output to both FFT widgets
        self.sig_tx_fft.connect(self.fft_widget.sig_rx)
        self.sig_tx_fft.connect(self.qfft_win_select.sig_rx)

        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        # --- run control ---
        self.led_N_start.editingFinished.connect(self.update_N)
        self.led_N_points.editingFinished.connect(self.update_N)
        self.led_N_frame.editingFinished.connect(self.update_N)
        self.but_fft_wdg.clicked.connect(self.toggle_fft_wdg)

    # -------------------------------------------------------------------------
    def update_N(self, emit=True):
        """
        Update values for `self.N` and `self.win_dict['N']`, for `self.N_start` and
        `self.N_end` from the corresponding QLineEditWidgets.
        When `emit==True`, fire `'ui_changed': 'N'` to update the FFT window and the
        `plot_impz` widgets. In contrast to `view_changed`, this also forces a
        recalculation of the transient response.

        This method is called by:

        - `self._construct_ui()` with `emit==False`
        - `plot_impz()` with `emit==False` when the automatic calculation
                of N has to be updated (e.g. order of FIR Filter has changed
        - signal-slot connection when `N_start` or `N_end` QLineEdit widgets have
                been changed (`emit==True`)
        """
        if not isinstance(emit, bool):
            logger.error("update N: emit={0}".format(emit))
        self.N_start = safe_eval(self.led_N_start.text(),
                                 self.N_start,
                                 return_type='int',
                                 sign='poszero')
        self.led_N_start.setText(str(self.N_start))  # update widget

        self.N_user = safe_eval(self.led_N_points.text(),
                                self.N_user,
                                return_type='int',
                                sign='poszero')

        if self.N_user == 0:  # automatic calculation
            self.N = self.calc_n_points(self.N_user)  # widget remains set to 0
            self.led_N_points.setText("0")  # update widget
        else:
            self.N = self.N_user
            self.led_N_points.setText(str(self.N))  # update widget

        # total number of points to be calculated: N + N_start
        self.N_end = self.N + self.N_start

        self.N_frame_user = safe_eval(self.led_N_frame.text(),
                                      self.N_frame_user,
                                      return_type='int',
                                      sign='poszero')

        if self.N_frame_user == 0:
            self.N_frame = self.N_end  # use N_end for frame length
            self.led_N_frame.setText(
                "0")  # update widget with "0" as set by user
        else:
            self.N_frame = self.N_frame_user
            self.led_N_frame.setText(str(self.N_frame))  # update widget

        # recalculate displayed freq. index values when freq. unit == 'k'
        if fb.fil[0]['freq_specs_unit'] == 'k':
            self.update_freqs()

        if emit:
            # use `'ui_changed'` as this triggers recalculation of the transient
            # response
            self.emit({'ui_changed': 'N'})

    # ------------------------------------------------------------------------------
    def toggle_fft_wdg(self):
        """
        Show / hide FFT widget depending on the state of the corresponding button
        When widget is shown, trigger an update of the window function.
        """
        if self.but_fft_wdg.isChecked():
            self.fft_widget.show()
            self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_fft')
        else:
            self.fft_widget.hide()

    # --------------------------------------------------------------------------
    def hide_fft_wdg(self):
        """
        The closeEvent caused by clicking the "x" in the FFT widget is caught
        there and routed here to only hide the window
        """
        self.but_fft_wdg.setChecked(False)
        self.fft_widget.hide()

    # ------------------------------------------------------------------------------
    def calc_n_points(self, N_user=0):
        """
        Calculate number of points to be displayed, depending on type of filter
        (FIR, IIR) and user input. If the user selects 0 points, the number is
        calculated automatically.

        An improvement would be to calculate the dominant pole and the corresponding
        settling time.
        """
        if N_user == 0:  # set number of data points automatically
            if fb.fil[0]['ft'] == 'IIR':
                # IIR: No algorithm yet, set N = 100
                N = 100
            else:
                # FIR: N = number of coefficients (max. 100)
                N = min(len(fb.fil[0]['ba'][0]), 100)
        else:
            N = N_user

        return N
Exemplo n.º 3
0
class UI_W(QWidget):
    """
    Widget for entering integer and fractional bits. The result can be read out
    via the attributes `self.WI`, `self.WF` and `self.W`.

    The constructor accepts a dictionary for initial widget settings.
    The following keys are defined; default values are used for missing keys:

    'wdg_name'      : 'ui_w'                    # widget name
    'label'         : 'WI.WF'                   # widget text label
    'visible'       : True                      # Is widget visible?
    'enabled'       : True                      # Is widget enabled?

    'fractional'    : True                      # Display WF, otherwise WF=0
    'lbl_sep'       : '.'                       # label between WI and WF field
    'max_led_width' : 30                        # max. length of lineedit field
    'WI'            : 0                         # number of frac. *bits*
    'WI_len'        : 2                         # max. number of integer *digits*
    'tip_WI'        : 'Number of integer bits'  # Mouse-over tooltip
    'WF'            : 15                        # number of frac. *bits*
    'WF_len'        : 2                         # max. number of frac. *digits*
    'tip_WF'        : 'Number of frac. bits'    # Mouse-over tooltip


    'lock_visible'  : False                     # Pushbutton for locking visible
    'tip_lock'      : 'Lock input/output quant.'# Tooltip for  lock push button

    'combo_visible' : False                     # Enable integrated combo widget
    'combo_items'   : ['auto', 'full', 'man']   # Combo selection
    'tip_combo'     : 'Calculate Acc. width.'   # tooltip for combo
    """
    # sig_rx = pyqtSignal(object)  # incoming,
    sig_tx = pyqtSignal(object)  # outcgoing
    from pyfda.libs.pyfda_qt_lib import emit

    def __init__(self, parent, q_dict, **kwargs):
        super(UI_W, self).__init__(parent)
        self.q_dict = q_dict  # pass a dict with initial settings for construction
        self._construct_UI(**kwargs)
        self.ui2dict(s='init')  # initialize the class attributes

    def _construct_UI(self, **kwargs):
        """
        Construct widget from quantization dict, individual settings and
        the default dict below """

        # default settings
        dict_ui = {
            'wdg_name': 'ui_w',
            'label': 'WI.WF',
            'lbl_sep': '.',
            'max_led_width': 30,
            'WI': 0,
            'WI_len': 2,
            'tip_WI': 'Number of integer bits',
            'WF': 15,
            'WF_len': 2,
            'tip_WF': 'Number of fractional bits',
            'enabled': True,
            'visible': True,
            'fractional': True,
            'combo_visible': False,
            'combo_items': ['auto', 'full', 'man'],
            'tip_combo': 'Calculate Acc. width.',
            'lock_visible': False,
            'tip_lock': 'Lock input/output quantization.'
        }  #: default values

        if self.q_dict:
            dict_ui.update(self.q_dict)

        for k, v in kwargs.items():
            if k not in dict_ui:
                logger.warning("Unknown key {0}".format(k))
            else:
                dict_ui.update({k: v})

        self.wdg_name = dict_ui['wdg_name']

        if not dict_ui['fractional']:
            dict_ui['WF'] = 0
        self.WI = dict_ui['WI']
        self.WF = dict_ui['WF']
        self.W = int(self.WI + self.WF + 1)
        if self.q_dict:
            self.q_dict.update({'WI': self.WI, 'WF': self.WF, 'W': self.W})
        else:
            self.q_dict = {'WI': self.WI, 'WF': self.WF, 'W': self.W}

        lblW = QLabel(to_html(dict_ui['label'], frmt='bi'), self)

        self.cmbW = QComboBox(self)
        self.cmbW.addItems(dict_ui['combo_items'])
        self.cmbW.setVisible(dict_ui['combo_visible'])
        self.cmbW.setToolTip(dict_ui['tip_combo'])
        self.cmbW.setObjectName("cmbW")

        self.butLock = QPushButton(self)
        self.butLock.setCheckable(True)
        self.butLock.setChecked(False)
        self.butLock.setVisible(dict_ui['lock_visible'])
        self.butLock.setToolTip(dict_ui['tip_lock'])

        self.ledWI = QLineEdit(self)
        self.ledWI.setToolTip(dict_ui['tip_WI'])
        self.ledWI.setMaxLength(dict_ui['WI_len'])  # maximum of 2 digits
        self.ledWI.setFixedWidth(
            dict_ui['max_led_width'])  # width of lineedit in points
        self.ledWI.setObjectName("WI")

        lblDot = QLabel(dict_ui['lbl_sep'], self)
        lblDot.setVisible(dict_ui['fractional'])

        self.ledWF = QLineEdit(self)
        self.ledWF.setToolTip(dict_ui['tip_WF'])
        self.ledWF.setMaxLength(dict_ui['WI_len'])  # maximum of 2 digits
        self.ledWF.setFixedWidth(
            dict_ui['max_led_width'])  # width of lineedit in points
        self.ledWF.setVisible(dict_ui['fractional'])
        self.ledWF.setObjectName("WF")

        layH = QHBoxLayout()
        layH.addWidget(lblW)
        layH.addStretch()
        layH.addWidget(self.cmbW)
        layH.addWidget(self.butLock)
        layH.addWidget(self.ledWI)
        layH.addWidget(lblDot)
        layH.addWidget(self.ledWF)
        layH.setContentsMargins(0, 0, 0, 0)

        frmMain = QFrame(self)
        frmMain.setLayout(layH)

        layVMain = QVBoxLayout()  # Widget main layout
        layVMain.addWidget(frmMain)
        layVMain.setContentsMargins(0, 5, 0, 0)  # *params['wdg_margins'])

        self.setLayout(layVMain)

        # ----------------------------------------------------------------------
        # INITIAL SETTINGS
        # ----------------------------------------------------------------------
        self.ledWI.setText(qstr(dict_ui['WI']))
        self.ledWF.setText(qstr(dict_ui['WF']))

        frmMain.setEnabled(dict_ui['enabled'])
        frmMain.setVisible(dict_ui['visible'])

        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.ledWI.editingFinished.connect(self.ui2dict)
        self.ledWF.editingFinished.connect(self.ui2dict)
        self.butLock.clicked.connect(self.butLock_clicked)
        self.cmbW.currentIndexChanged.connect(self.ui2dict)

        # initialize button icon
        self.butLock_clicked(self.butLock.isChecked())

    def quant_coeffs(self,
                     q_dict: dict,
                     coeffs: iterable,
                     to_int: bool = False) -> list:
        """
        Quantize the coefficients, scale and convert them to integer and return them
        as a list of integers

        This is called every time one of the coefficient subwidgets is edited or changed.

        Parameters:
        -----------
        q_dict: dict
           Dictionary with quantizer settings for coefficients

        coeffs: iterable
           a list or ndarray of coefficients to be quantized

        Returns:
        --------
        A list of integer coeffcients, quantized and scaled with the settings
        of the passed quantization dict

        """
        # Create coefficient quantizer instance using the passed quantization parameters
        # dict from `input_widgets/input_coeffs.py` (and stored in the central
        # filter dict)
        Q_coeff = fx.Fixed(q_dict)
        Q_coeff.frmt = 'dec'  # always use decimal format for coefficients

        if coeffs is None:
            logger.error("Coeffs empty!")
        # quantize floating point coefficients with the selected scale (WI.WF),
        # next convert array float  -> array of fixp
        #                           -> list of int (scaled by 2^WF) when `to_int == True`
        if to_int:
            return list(Q_coeff.float2frmt(coeffs) * (1 << Q_coeff.WF))
        else:
            return list(Q_coeff.fixp(coeffs))

    # --------------------------------------------------------------------------
    def butLock_clicked(self, clicked):
        """
        Update the icon of the push button depending on its state
        """
        if clicked:
            self.butLock.setIcon(QIcon(':/lock-locked.svg'))
        else:
            self.butLock.setIcon(QIcon(':/lock-unlocked.svg'))

        q_icon_size = self.butLock.iconSize(
        )  # <- uncomment this for manual sizing
        self.butLock.setIconSize(q_icon_size)

        dict_sig = {'wdg_name': self.wdg_name, 'ui': 'butLock'}
        self.emit(dict_sig)

    # --------------------------------------------------------------------------
    def ui2dict(self, s=None):
        """
        Update the attributes `self.WI`, `self.WF` and `self.W` and `self.q_dict`
        when one of the QLineEdit widgets has been edited.

        Emit a signal with `{'ui':objectName of the sender}`.
        """

        self.WI = int(
            safe_eval(self.ledWI.text(),
                      self.WI,
                      return_type="int",
                      sign='poszero'))
        self.ledWI.setText(qstr(self.WI))
        self.WF = int(
            safe_eval(self.ledWF.text(),
                      self.WF,
                      return_type="int",
                      sign='poszero'))
        self.ledWF.setText(qstr(self.WF))
        self.W = int(self.WI + self.WF + 1)

        self.q_dict.update({'WI': self.WI, 'WF': self.WF, 'W': self.W})

        if self.sender():
            obj_name = self.sender().objectName()
            logger.debug("sender: {0}".format(obj_name))
            dict_sig = {'wdg_name': self.wdg_name, 'ui': obj_name}
            self.emit(dict_sig)
        elif s == 'init':
            logger.debug("called by __init__")
        else:
            logger.error("sender without name!")

    # --------------------------------------------------------------------------
    def dict2ui(self, q_dict=None):
        """
        Update the widgets `WI` and `WF` and the corresponding attributes
        from the dict passed as the argument
        """
        if q_dict is None:
            q_dict = self.q_dict

        if 'WI' in q_dict:
            self.WI = safe_eval(q_dict['WI'],
                                self.WI,
                                return_type="int",
                                sign='poszero')
            self.ledWI.setText(qstr(self.WI))
        else:
            logger.warning("No key 'WI' in dict!")

        if 'WF' in q_dict:
            self.WF = safe_eval(q_dict['WF'],
                                self.WF,
                                return_type="int",
                                sign='poszero')
            self.ledWF.setText(qstr(self.WF))
        else:
            logger.warning("No key 'WF' in dict!")

        self.W = self.WF + self.WI + 1
Exemplo n.º 4
0
class Delay(QWidget):

    FRMT = 'zpk' # output format of delay filter widget

    info ="""
**Delay widget**

allows entering the number of **delays** :math:`N` :math:`T_S`. It is treated as a FIR filter,
the number of delays is directly translated to a number of poles (:math:`N > 0`) 
or zeros (:math:`N < 0`).

Obviously, there is no minimum design algorithm or no design algorithm at all :-)

    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.N = 5

        self.ft = 'FIR'
        
        self.rt_dicts = ('com',)

        self.rt_dict = {
            'COM': {'man': {'fo':('a', 'N'),
                            'msg':('a', 
                                "<span>Enter desired filter order <b><i>N</i></b>, corner "
                                "frequencies of pass and stop band(s), <b><i>F<sub>PB</sub></i></b>"
                                "&nbsp; and <b><i>F<sub>SB</sub></i></b>&nbsp;, and relative weight "
                                "values <b><i>W&nbsp; </i></b> (1 ... 10<sup>6</sup>) to specify how well "
                                "the bands are approximated.</span>")
                            },
                },
            'LP': {'man':{'wspecs': ('u','W_PB','W_SB'),
                          'tspecs': ('u', {'frq':('a','F_PB','F_SB'), 
                                           'amp':('u','A_PB','A_SB')})
                          },
                },
            'HP': {'man':{'wspecs': ('u','W_SB','W_PB')},
                    },
            'BP': {
                    },
            'BS': {'man':{'wspecs': ('u','W_PB','W_SB','W_PB2'),
                          'tspecs': ('u', {'frq':('a','F_PB','F_SB','F_SB2','F_PB2'), 
                                           'amp':('u','A_PB','A_SB','A_PB2')})
                          }
                }
            }

        self.info_doc = []

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in 
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        self.lbl_delay = QLabel("Delays", self)
        self.lbl_delay.setObjectName('wdg_lbl_delays')
        self.led_delay = QLineEdit(self)
        self.led_delay.setText(str(self.N))
        self.led_delay.setObjectName('wdg_led_delay')
        self.led_delay.setToolTip("Number of delays, N > 0 produces poles, N < 0 zeros.")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_delay)
        self.layHWin.addWidget(self.led_delay)
        self.layHWin.setContentsMargins(0,0,0,0)
        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.led_delay.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict() # get initial / last setting from dictionary
        self._update_UI()
        
    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.N = safe_eval(self.led_delay.text(), self.N, 
                                      return_type='int')
        self.led_delay.setText(str(self.N))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil':{}})
        fb.fil[0]['wdg_fil'].update({'delay':
                                        {'N':self.N}
                                    })
        
        # sig_tx -> select_filter -> filter_specs   
        self.sig_tx.emit({'sender':__name__, 'filt_changed':'delay'})


    def _load_dict(self):
        """
        Reload parameter(s) from filter dictionary (if they exist) and set 
        corresponding UI elements. _load_dict() is called upon initialization
        and when the filter is loaded from disk.
        """
        if 'wdg_fil' in fb.fil[0] and 'delay' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['delay']
            if 'N' in wdg_fil_par:
                self.N = wdg_fil_par['N']
                self.led_delay.setText(str(self.N))


    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N     = fil_dict['N']  # filter order is translated to numb. of delays
                                        
        
    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 2000:
            return qfilter_warning(self, self.N, "Delay")
        else:
            return True

    def _save(self, fil_dict, arg=None):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        if arg is None:
            arg = np.zeros(self.N)
        fil_save(fil_dict, arg, self.FRMT, __name__)

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)


    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)
        
    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)
Exemplo n.º 5
0
class PlotImpz_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming: not implemented at the moment, update_N is triggered directly
    # by plot_impz
    # sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)
    # outgoing to local fft window
    sig_tx_fft = pyqtSignal(object)


    def __init__(self, parent):
        """
        Pass instance `parent` of parent class (FilterCoeffs)
        """
        super(PlotImpz_UI, self).__init__(parent)

        """
        Intitialize the widget, consisting of:
        - top chkbox row
        - coefficient table
        - two bottom rows with action buttons
        """

        # initial settings
        self.N_start = 0
        self.N_user = 0
        self.N = 0

        # time
        self.plt_time_resp = "Stem"
        self.plt_time_stim = "None"
        self.plt_time_stmq = "None"
        self.plt_time_spgr = "None"

        self.bottom_t = -80 # initial value for log. scale (time)
        self.nfft_spgr_time = 256 # number of fft points per spectrogram segment
        self.ovlp_spgr_time = 128 # number of overlap points between spectrogram segments
        self.mode_spgr_time = "magnitude"

        # stimuli
        self.stim = "Impulse"
        self.chirp_method = 'Linear'
        self.noise = "None"

        self.f1 = 0.02
        self.f2 = 0.03
        self.A1 = 1.0
        self.A2 = 0.0
        self.phi1 = self.phi2 = 0
        self.noi = 0.1
        self.noise = 'none'
        self.DC = 0.0
        self.stim_formula = "A1 * abs(sin(2 * pi * f1 * n))"

        # frequency
        self.plt_freq_resp = "Line"
        self.plt_freq_stim = "None"
        self.plt_freq_stmq = "None"

        self.bottom_f = -120 # initial value for log. scale
        self.param = None


        # dictionary for fft window settings
        self.win_dict = fb.fil[0]['win_fft']
        self.fft_window = None # handle for FFT window pop-up widget
        self.window_name = "Rectangular"

        self._construct_UI()
        self._enable_stim_widgets()
        self.update_N(emit=False) # also updates window function
        self._update_noi()


    def _construct_UI(self):
        # ----------- ---------------------------------------------------
        # Run control widgets
        # ---------------------------------------------------------------
        self.chk_auto_run = QCheckBox("Auto", self)
        self.chk_auto_run.setObjectName("chk_auto_run")
        self.chk_auto_run.setToolTip("<span>Update response automatically when "
                                     "parameters have been changed.</span>")
        self.chk_auto_run.setChecked(True)

        self.but_run = QPushButton(self)
        self.but_run.setText("RUN")
        self.but_run.setToolTip("Run simulation")
        self.but_run.setEnabled(not self.chk_auto_run.isChecked())

        self.cmb_sim_select = QComboBox(self)
        self.cmb_sim_select.addItems(["Float","Fixpoint"])
        qset_cmb_box(self.cmb_sim_select, "Float")
        self.cmb_sim_select.setToolTip("<span>Simulate floating-point or fixpoint response."
                                 "</span>")

        self.lbl_N_points = QLabel(to_html("N", frmt='bi')  + " =", self)
        self.led_N_points = QLineEdit(self)
        self.led_N_points.setText(str(self.N))
        self.led_N_points.setToolTip("<span>Number of displayed data points. "
                                   "<i>N</i> = 0 tries to choose for you.</span>")

        self.lbl_N_start = QLabel(to_html("N_0", frmt='bi') + " =", self)
        self.led_N_start = QLineEdit(self)
        self.led_N_start.setText(str(self.N_start))
        self.led_N_start.setToolTip("<span>First point to plot.</span>")

        self.chk_fx_scale = QCheckBox("Int. scale", self)
        self.chk_fx_scale.setObjectName("chk_fx_scale")
        self.chk_fx_scale.setToolTip("<span>Display data with integer (fixpoint) scale.</span>")
        self.chk_fx_scale.setChecked(False)

        self.chk_stim_options = QCheckBox("Stim. Options", self)
        self.chk_stim_options.setObjectName("chk_stim_options")
        self.chk_stim_options.setToolTip("<span>Show stimulus options.</span>")
        self.chk_stim_options.setChecked(True)

        self.but_fft_win = QPushButton(self)
        self.but_fft_win.setText("WIN FFT")
        self.but_fft_win.setToolTip('<span> time and frequency response of FFT Window '
                                    '(can be modified in the "Frequency" tab)</span>')
        self.but_fft_win.setCheckable(True)
        self.but_fft_win.setChecked(False)

        layH_ctrl_run = QHBoxLayout()
        layH_ctrl_run.addWidget(self.but_run)
        #layH_ctrl_run.addWidget(self.lbl_sim_select)
        layH_ctrl_run.addWidget(self.cmb_sim_select)
        layH_ctrl_run.addWidget(self.chk_auto_run)
        layH_ctrl_run.addStretch(1)
        layH_ctrl_run.addWidget(self.lbl_N_start)
        layH_ctrl_run.addWidget(self.led_N_start)
        layH_ctrl_run.addStretch(1)
        layH_ctrl_run.addWidget(self.lbl_N_points)
        layH_ctrl_run.addWidget(self.led_N_points)
        layH_ctrl_run.addStretch(2)
        layH_ctrl_run.addWidget(self.chk_fx_scale)
        layH_ctrl_run.addStretch(2)
        layH_ctrl_run.addWidget(self.chk_stim_options)
        layH_ctrl_run.addStretch(2)
        layH_ctrl_run.addWidget(self.but_fft_win)
        layH_ctrl_run.addStretch(10)

        #layH_ctrl_run.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_run = QWidget(self)
        self.wdg_ctrl_run.setLayout(layH_ctrl_run)
        # --- end of run control ----------------------------------------

        # ----------- ---------------------------------------------------
        # Controls for time domain
        # ---------------------------------------------------------------
        plot_styles_list = ["None","Dots","Line","Line*","Stem","Stem*","Step","Step*"]

        lbl_plt_time_title = QLabel("<b>View:</b>", self)

        self.lbl_plt_time_stim = QLabel(to_html("Stimulus x", frmt='bi'), self)
        self.cmb_plt_time_stim = QComboBox(self)
        self.cmb_plt_time_stim.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_time_stim, self.plt_time_stim)
        self.cmb_plt_time_stim.setToolTip("<span>Plot style for stimulus.</span>")

        self.lbl_plt_time_stmq = QLabel(to_html("&nbsp;&nbsp;Fixp. Stim. x_Q", frmt='bi'), self)
        self.cmb_plt_time_stmq = QComboBox(self)
        self.cmb_plt_time_stmq.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_time_stmq, self.plt_time_stmq)
        self.cmb_plt_time_stmq.setToolTip("<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>")

        lbl_plt_time_resp = QLabel(to_html("&nbsp;&nbsp;Response y", frmt='bi'), self)
        self.cmb_plt_time_resp = QComboBox(self)
        self.cmb_plt_time_resp.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_time_resp, self.plt_time_resp)
        self.cmb_plt_time_resp.setToolTip("<span>Plot style for response.</span>")

        lbl_win_time = QLabel(to_html("&nbsp;&nbsp;FFT Window", frmt='bi'), self)
        self.chk_win_time = QCheckBox(self)
        self.chk_win_time.setObjectName("chk_win_time")
        self.chk_win_time.setToolTip('<span>Show FFT windowing function (can be modified in the "Frequency" tab).</span>')
        self.chk_win_time.setChecked(False)

        lbl_log_time = QLabel(to_html("dB", frmt='b'), self)
        self.chk_log_time = QCheckBox(self)
        self.chk_log_time.setObjectName("chk_log_time")
        self.chk_log_time.setToolTip("<span>Logarithmic scale for y-axis.</span>")
        self.chk_log_time.setChecked(False)

        self.lbl_log_bottom_time = QLabel(to_html("min =", frmt='bi'), self)
        self.lbl_log_bottom_time.setVisible(True)
        self.led_log_bottom_time = QLineEdit(self)
        self.led_log_bottom_time.setText(str(self.bottom_t))
        self.led_log_bottom_time.setToolTip("<span>Minimum display value for time "
                                            "and spectrogram plots with log. scale.</span>")
        self.led_log_bottom_time.setVisible(True)

        lbl_plt_time_spgr = QLabel(to_html("&nbsp;&nbsp;Spectrogram", frmt='bi'), self)
        self.cmb_plt_time_spgr = QComboBox(self)
        self.cmb_plt_time_spgr.addItems(["None", "x[n]", "x_q[n]", "y[n]"])
        qset_cmb_box(self.cmb_plt_time_spgr, self.plt_time_spgr)
        self.cmb_plt_time_spgr.setToolTip("<span>Show Spectrogram for selected signal.</span>")
        spgr_en = self.plt_time_spgr != "None"

        self.lbl_log_spgr_time = QLabel(to_html("&nbsp;dB", frmt='b'), self)
        self.lbl_log_spgr_time.setVisible(spgr_en)
        self.chk_log_spgr_time = QCheckBox(self)
        self.chk_log_spgr_time.setObjectName("chk_log_spgr")
        self.chk_log_spgr_time.setToolTip("<span>Logarithmic scale for spectrogram.</span>")
        self.chk_log_spgr_time.setChecked(True)
        self.chk_log_spgr_time.setVisible(spgr_en)

        self.lbl_nfft_spgr_time = QLabel(to_html("&nbsp;N_FFT =", frmt='bi'), self)
        self.lbl_nfft_spgr_time.setVisible(spgr_en)
        self.led_nfft_spgr_time = QLineEdit(self)
        self.led_nfft_spgr_time.setText(str(self.nfft_spgr_time))
        self.led_nfft_spgr_time.setToolTip("<span>Number of FFT points per spectrogram segment.</span>")
        self.led_nfft_spgr_time.setVisible(spgr_en)

        self.lbl_ovlp_spgr_time = QLabel(to_html("&nbsp;N_OVLP =", frmt='bi'), self)
        self.lbl_ovlp_spgr_time.setVisible(spgr_en)
        self.led_ovlp_spgr_time = QLineEdit(self)
        self.led_ovlp_spgr_time.setText(str(self.ovlp_spgr_time))
        self.led_ovlp_spgr_time.setToolTip("<span>Number of overlap data points between spectrogram segments.</span>")
        self.led_ovlp_spgr_time.setVisible(spgr_en)

        self.lbl_mode_spgr_time = QLabel(to_html("&nbsp;Mode", frmt='bi'), self)
        self.lbl_mode_spgr_time.setVisible(spgr_en)
        self.cmb_mode_spgr_time = QComboBox(self)
        spgr_modes = [("PSD","psd"), ("Mag.","magnitude"),\
                      ("Angle","angle"), ("Phase","phase")]
        for i in spgr_modes:
            self.cmb_mode_spgr_time.addItem(*i)
        qset_cmb_box(self.cmb_mode_spgr_time, self.mode_spgr_time, data=True)
        self.cmb_mode_spgr_time.setToolTip("<span>Spectrogram display mode.</span>")
        self.cmb_mode_spgr_time.setVisible(spgr_en)

        self.lbl_byfs_spgr_time = QLabel(to_html("&nbsp;per f_S", frmt='b'), self)
        self.lbl_byfs_spgr_time.setVisible(spgr_en)
        self.chk_byfs_spgr_time = QCheckBox(self)
        self.chk_byfs_spgr_time.setObjectName("chk_log_spgr")
        self.chk_byfs_spgr_time.setToolTip("<span>Display spectral density i.e. scale by f_S</span>")
        self.chk_byfs_spgr_time.setChecked(True)
        self.chk_byfs_spgr_time.setVisible(spgr_en)


        # self.lbl_colorbar_time = QLabel(to_html("&nbsp;Col.bar", frmt='b'), self)
        # self.lbl_colorbar_time.setVisible(spgr_en)
        # self.chk_colorbar_time = QCheckBox(self)
        # self.chk_colorbar_time.setObjectName("chk_colorbar_time")
        # self.chk_colorbar_time.setToolTip("<span>Enable colorbar</span>")
        # self.chk_colorbar_time.setChecked(True)
        # self.chk_colorbar_time.setVisible(spgr_en)

        self.chk_fx_limits = QCheckBox("Min/max.", self)
        self.chk_fx_limits.setObjectName("chk_fx_limits")
        self.chk_fx_limits.setToolTip("<span>Display limits of fixpoint range.</span>")
        self.chk_fx_limits.setChecked(False)

        layH_ctrl_time = QHBoxLayout()
        layH_ctrl_time.addWidget(lbl_plt_time_title)
        layH_ctrl_time.addStretch(1)
        layH_ctrl_time.addWidget(self.lbl_plt_time_stim)
        layH_ctrl_time.addWidget(self.cmb_plt_time_stim)
        #
        layH_ctrl_time.addWidget(self.lbl_plt_time_stmq)
        layH_ctrl_time.addWidget(self.cmb_plt_time_stmq)
        #
        layH_ctrl_time.addWidget(lbl_plt_time_resp)
        layH_ctrl_time.addWidget(self.cmb_plt_time_resp)
        #
        layH_ctrl_time.addWidget(lbl_win_time)
        layH_ctrl_time.addWidget(self.chk_win_time)
        layH_ctrl_time.addStretch(1)
        layH_ctrl_time.addWidget(lbl_log_time)
        layH_ctrl_time.addWidget(self.chk_log_time)
        layH_ctrl_time.addWidget(self.lbl_log_bottom_time)
        layH_ctrl_time.addWidget(self.led_log_bottom_time)
        #
        layH_ctrl_time.addStretch(1)
        #
        layH_ctrl_time.addWidget(lbl_plt_time_spgr)
        layH_ctrl_time.addWidget(self.cmb_plt_time_spgr)
        layH_ctrl_time.addWidget(self.lbl_log_spgr_time)
        layH_ctrl_time.addWidget(self.chk_log_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_nfft_spgr_time)
        layH_ctrl_time.addWidget(self.led_nfft_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_ovlp_spgr_time)
        layH_ctrl_time.addWidget(self.led_ovlp_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_mode_spgr_time)
        layH_ctrl_time.addWidget(self.cmb_mode_spgr_time)
        layH_ctrl_time.addWidget(self.lbl_byfs_spgr_time)
        layH_ctrl_time.addWidget(self.chk_byfs_spgr_time)

        layH_ctrl_time.addStretch(2)
        layH_ctrl_time.addWidget(self.chk_fx_limits)
        layH_ctrl_time.addStretch(10)

        #layH_ctrl_time.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_time = QWidget(self)
        self.wdg_ctrl_time.setLayout(layH_ctrl_time)
        # ---- end time domain ------------------

        # ---------------------------------------------------------------
        # Controls for frequency domain
        # ---------------------------------------------------------------
        lbl_plt_freq_title = QLabel("<b>View:</b>", self)

        self.lbl_plt_freq_stim = QLabel(to_html("Stimulus X", frmt='bi'), self)
        self.cmb_plt_freq_stim = QComboBox(self)
        self.cmb_plt_freq_stim.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_freq_stim, self.plt_freq_stim)
        self.cmb_plt_freq_stim.setToolTip("<span>Plot style for stimulus.</span>")

        self.lbl_plt_freq_stmq = QLabel(to_html("&nbsp;Fixp. Stim. X_Q", frmt='bi'), self)
        self.cmb_plt_freq_stmq = QComboBox(self)
        self.cmb_plt_freq_stmq.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_freq_stmq, self.plt_freq_stmq)
        self.cmb_plt_freq_stmq.setToolTip("<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>")

        lbl_plt_freq_resp = QLabel(to_html("&nbsp;Response Y", frmt='bi'), self)
        self.cmb_plt_freq_resp = QComboBox(self)
        self.cmb_plt_freq_resp.addItems(plot_styles_list)
        qset_cmb_box(self.cmb_plt_freq_resp, self.plt_freq_resp)
        self.cmb_plt_freq_resp.setToolTip("<span>Plot style for response.</span>")

        lbl_log_freq = QLabel(to_html("dB", frmt='b'), self)
        self.chk_log_freq = QCheckBox(self)
        self.chk_log_freq.setObjectName("chk_log_freq")
        self.chk_log_freq.setToolTip("<span>Logarithmic scale for y-axis.</span>")
        self.chk_log_freq.setChecked(True)

        self.lbl_log_bottom_freq = QLabel(to_html("min =", frmt='bi'), self)
        self.lbl_log_bottom_freq.setVisible(self.chk_log_freq.isChecked())
        self.led_log_bottom_freq = QLineEdit(self)
        self.led_log_bottom_freq.setText(str(self.bottom_f))
        self.led_log_bottom_freq.setToolTip("<span>Minimum display value for log. scale.</span>")
        self.led_log_bottom_freq.setVisible(self.chk_log_freq.isChecked())

        if not self.chk_log_freq.isChecked():
            self.bottom_f = 0
            
        lbl_re_im_freq = QLabel(to_html("Re / Im", frmt='b'), self)
        self.chk_re_im_freq = QCheckBox(self)
        self.chk_re_im_freq.setObjectName("chk_re_im_freq")
        self.chk_re_im_freq.setToolTip("<span>Show real and imaginary part of spectrum</span>")
        self.chk_re_im_freq.setChecked(False)

        self.lbl_win_fft = QLabel(to_html("Window", frmt='bi'), self)
        self.cmb_win_fft = QComboBox(self)
        self.cmb_win_fft.addItems(get_window_names())
        self.cmb_win_fft.setToolTip("FFT window type.")
        qset_cmb_box(self.cmb_win_fft, self.window_name)

        self.cmb_win_fft_variant = QComboBox(self)
        self.cmb_win_fft_variant.setToolTip("FFT window variant.")
        self.cmb_win_fft_variant.setVisible(False)

        self.lblWinPar1 = QLabel("Param1")
        self.ledWinPar1 = QLineEdit(self)
        self.ledWinPar1.setText("1")
        self.ledWinPar1.setObjectName("ledWinPar1")

        self.lblWinPar2 = QLabel("Param2")
        self.ledWinPar2 = QLineEdit(self)
        self.ledWinPar2.setText("2")
        self.ledWinPar2.setObjectName("ledWinPar2")

        self.chk_Hf = QCheckBox(self)
        self.chk_Hf.setObjectName("chk_Hf")
        self.chk_Hf.setToolTip("<span>Show ideal frequency response, calculated "
                               "from the filter coefficients.</span>")
        self.chk_Hf.setChecked(False)
        self.chk_Hf_lbl = QLabel(to_html("H_id (f)", frmt="bi"), self)

        lbl_show_info_freq = QLabel(to_html("Info", frmt='b'), self)
        self.chk_show_info_freq = QCheckBox(self)
        self.chk_show_info_freq.setObjectName("chk_show_info_freq")
        self.chk_show_info_freq.setToolTip("<span>Show infos about signal power "
                                           "and window properties.</span>")
        self.chk_show_info_freq.setChecked(False)

        layH_ctrl_freq = QHBoxLayout()
        layH_ctrl_freq.addWidget(lbl_plt_freq_title)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(self.lbl_plt_freq_stim)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_stim)
        #
        layH_ctrl_freq.addWidget(self.lbl_plt_freq_stmq)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_stmq)
        #
        layH_ctrl_freq.addWidget(lbl_plt_freq_resp)
        layH_ctrl_freq.addWidget(self.cmb_plt_freq_resp)
        #
        layH_ctrl_freq.addWidget(self.chk_Hf_lbl)
        layH_ctrl_freq.addWidget(self.chk_Hf)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(lbl_log_freq)
        layH_ctrl_freq.addWidget(self.chk_log_freq)
        layH_ctrl_freq.addWidget(self.lbl_log_bottom_freq)
        layH_ctrl_freq.addWidget(self.led_log_bottom_freq)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(lbl_re_im_freq)
        layH_ctrl_freq.addWidget(self.chk_re_im_freq)
        layH_ctrl_freq.addStretch(2)
        layH_ctrl_freq.addWidget(self.lbl_win_fft)
        layH_ctrl_freq.addWidget(self.cmb_win_fft)
        layH_ctrl_freq.addWidget(self.cmb_win_fft_variant)
        layH_ctrl_freq.addWidget(self.lblWinPar1)
        layH_ctrl_freq.addWidget(self.ledWinPar1)
        layH_ctrl_freq.addWidget(self.lblWinPar2)
        layH_ctrl_freq.addWidget(self.ledWinPar2)
        layH_ctrl_freq.addStretch(1)
        layH_ctrl_freq.addWidget(lbl_show_info_freq)
        layH_ctrl_freq.addWidget(self.chk_show_info_freq)
        layH_ctrl_freq.addStretch(10)

        #layH_ctrl_freq.setContentsMargins(*params['wdg_margins'])

        self.wdg_ctrl_freq = QWidget(self)
        self.wdg_ctrl_freq.setLayout(layH_ctrl_freq)
        # ---- end Frequency Domain ------------------

        # ---------------------------------------------------------------
        # Controls for stimuli
        # ---------------------------------------------------------------

        lbl_title_stim = QLabel("<b>Stimulus:</b>", self)

        self.lblStimulus = QLabel(to_html("Type", frmt='bi'), self)
        self.cmbStimulus = QComboBox(self)
        self.cmbStimulus.addItems(["None","Impulse","Step","StepErr","Cos","Sine", "Chirp",
                                   "Triang","Saw","Rect","Comb","AM","PM / FM","Formula"])
        self.cmbStimulus.setToolTip("Stimulus type.")
        qset_cmb_box(self.cmbStimulus, self.stim)

        self.chk_stim_bl = QCheckBox("BL", self)
        self.chk_stim_bl.setToolTip("<span>The signal is bandlimited to the Nyquist frequency "
                                    "to avoid aliasing. However, it is much slower to generate "
                                    "than the regular version.</span>")
        self.chk_stim_bl.setChecked(True)
        self.chk_stim_bl.setObjectName("stim_bl")

        self.cmbChirpMethod = QComboBox(self)
        for t in [("Lin","Linear"),("Square","Quadratic"),("Log", "Logarithmic"), ("Hyper", "Hyperbolic")]:
            self.cmbChirpMethod.addItem(*t)
        qset_cmb_box(self.cmbChirpMethod, self.chirp_method, data=False)

        self.chk_scale_impz_f = QCheckBox("Scale", self)
        self.chk_scale_impz_f.setToolTip("<span>Scale the FFT of the impulse response with <i>N<sub>FFT</sub></i> "
                                    "so that it has the same magnitude as |H(f)|. DC and Noise need to be "
                                    "turned off.</span>")
        self.chk_scale_impz_f.setChecked(True)
        self.chk_scale_impz_f.setObjectName("scale_impz_f")

        self.lblDC = QLabel(to_html("DC =", frmt='bi'), self)
        self.ledDC = QLineEdit(self)
        self.ledDC.setText(str(self.DC))
        self.ledDC.setToolTip("DC Level")
        self.ledDC.setObjectName("stimDC")

        layHCmbStim = QHBoxLayout()
        layHCmbStim.addWidget(self.cmbStimulus)
        layHCmbStim.addWidget(self.chk_stim_bl)
        layHCmbStim.addWidget(self.chk_scale_impz_f)
        layHCmbStim.addWidget(self.cmbChirpMethod)

        #----------------------------------------------
        self.lblAmp1 = QLabel(to_html("&nbsp;A_1", frmt='bi') + " =", self)
        self.ledAmp1 = QLineEdit(self)
        self.ledAmp1.setText(str(self.A1))
        self.ledAmp1.setToolTip("Stimulus amplitude, complex values like 3j - 1 are allowed")
        self.ledAmp1.setObjectName("stimAmp1")

        self.lblAmp2 = QLabel(to_html("&nbsp;A_2", frmt='bi') + " =", self)
        self.ledAmp2 = QLineEdit(self)
        self.ledAmp2.setText(str(self.A2))
        self.ledAmp2.setToolTip("Stimulus amplitude 2, complex values like 3j - 1 are allowed")
        self.ledAmp2.setObjectName("stimAmp2")

        #----------------------------------------------
        self.lblPhi1 = QLabel(to_html("&nbsp;&phi;_1", frmt='bi') + " =", self)
        self.ledPhi1 = QLineEdit(self)
        self.ledPhi1.setText(str(self.phi1))
        self.ledPhi1.setToolTip("Stimulus phase")
        self.ledPhi1.setObjectName("stimPhi1")
        self.lblPhU1 = QLabel(to_html("&deg;", frmt='b'), self)

        self.lblPhi2 = QLabel(to_html("&nbsp;&phi;_2", frmt='bi') + " =", self)
        self.ledPhi2 = QLineEdit(self)
        self.ledPhi2.setText(str(self.phi2))
        self.ledPhi2.setToolTip("Stimulus phase 2")
        self.ledPhi2.setObjectName("stimPhi2")
        self.lblPhU2 = QLabel(to_html("&deg;", frmt='b'), self)

        #----------------------------------------------
        self.lblFreq1 = QLabel(to_html("&nbsp;f_1", frmt='bi') + " =", self)
        self.ledFreq1 = QLineEdit(self)
        self.ledFreq1.setText(str(self.f1))
        self.ledFreq1.setToolTip("Stimulus frequency 1")
        self.ledFreq1.setObjectName("stimFreq1")
        self.lblFreqUnit1 = QLabel("f_S", self)

        self.lblFreq2 = QLabel(to_html("&nbsp;f_2", frmt='bi') + " =", self)
        self.ledFreq2 = QLineEdit(self)
        self.ledFreq2.setText(str(self.f2))
        self.ledFreq2.setToolTip("Stimulus frequency 2")
        self.ledFreq2.setObjectName("stimFreq2")
        self.lblFreqUnit2 = QLabel("f_S", self)
        
        #----------------------------------------------
        self.lblNoise = QLabel(to_html("&nbsp;Noise", frmt='bi'), self)
        self.cmbNoise = QComboBox(self)
        self.cmbNoise.addItems(["None","Gauss","Uniform","PRBS"])
        self.cmbNoise.setToolTip("Type of additive noise.")
        qset_cmb_box(self.cmbNoise, self.noise)

        self.lblNoi = QLabel("not initialized", self)
        self.ledNoi = QLineEdit(self)
        self.ledNoi.setText(str(self.noi))
        self.ledNoi.setToolTip("not initialized")
        self.ledNoi.setObjectName("stimNoi")
        
        layGStim = QGridLayout()
        
        layGStim.addWidget(self.lblStimulus, 0, 0)
        layGStim.addWidget(self.lblDC, 1, 0)

        layGStim.addLayout(layHCmbStim, 0, 1)
        layGStim.addWidget(self.ledDC,  1, 1)

        layGStim.addWidget(self.lblAmp1, 0, 2)
        layGStim.addWidget(self.lblAmp2, 1, 2)

        layGStim.addWidget(self.ledAmp1, 0, 3)
        layGStim.addWidget(self.ledAmp2, 1, 3)
        
        layGStim.addWidget(self.lblPhi1, 0, 4)
        layGStim.addWidget(self.lblPhi2, 1, 4)

        layGStim.addWidget(self.ledPhi1, 0, 5)
        layGStim.addWidget(self.ledPhi2, 1, 5)

        layGStim.addWidget(self.lblPhU1, 0, 6)
        layGStim.addWidget(self.lblPhU2, 1, 6)

        layGStim.addWidget(self.lblFreq1, 0, 7)
        layGStim.addWidget(self.lblFreq2, 1, 7)

        layGStim.addWidget(self.ledFreq1, 0, 8)
        layGStim.addWidget(self.ledFreq2, 1, 8)

        layGStim.addWidget(self.lblFreqUnit1, 0, 9)
        layGStim.addWidget(self.lblFreqUnit2, 1, 9)
        
        layGStim.addWidget(self.lblNoise, 0, 10)
        layGStim.addWidget(self.lblNoi, 1, 10)

        layGStim.addWidget(self.cmbNoise, 0, 11)
        layGStim.addWidget(self.ledNoi, 1, 11)

        #----------------------------------------------
        self.lblStimFormula = QLabel(to_html("x =", frmt='bi'), self)
        self.ledStimFormula = QLineEdit(self)
        self.ledStimFormula.setText(str(self.stim_formula))
        self.ledStimFormula.setToolTip("<span>Enter formula for stimulus in numexpr syntax"
                                  "</span>")
        self.ledStimFormula.setObjectName("stimFormula")

        layH_ctrl_stim_formula = QHBoxLayout()
        layH_ctrl_stim_formula.addWidget(self.lblStimFormula)
        layH_ctrl_stim_formula.addWidget(self.ledStimFormula,10)

        #----------------------------------------------
        #layG_ctrl_stim = QGridLayout()
        layH_ctrl_stim_par = QHBoxLayout()

        layH_ctrl_stim_par.addLayout(layGStim)

        layV_ctrl_stim = QVBoxLayout()
        layV_ctrl_stim.addLayout(layH_ctrl_stim_par)
        layV_ctrl_stim.addLayout(layH_ctrl_stim_formula)

        layH_ctrl_stim = QHBoxLayout()
        layH_ctrl_stim.addWidget(lbl_title_stim)
        layH_ctrl_stim.addStretch(1)
        layH_ctrl_stim.addLayout(layV_ctrl_stim)
        layH_ctrl_stim.addStretch(10)

        self.wdg_ctrl_stim = QWidget(self)
        self.wdg_ctrl_stim.setLayout(layH_ctrl_stim)
        # --------- end stimuli ---------------------------------

        # frequency widgets require special handling as they are scaled with f_s
        self.ledFreq1.installEventFilter(self)
        self.ledFreq2.installEventFilter(self)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        # --- run control ---
        self.led_N_start.editingFinished.connect(self.update_N)
        self.led_N_points.editingFinished.connect(self.update_N)

        # --- frequency control ---
        # careful! currentIndexChanged passes the current index to _update_win_fft
        self.cmb_win_fft.currentIndexChanged.connect(self._update_win_fft)
        self.ledWinPar1.editingFinished.connect(self._read_param1)
        self.ledWinPar2.editingFinished.connect(self._read_param2)

        # --- stimulus control ---
        self.chk_stim_options.clicked.connect(self._show_stim_options)

        self.chk_stim_bl.clicked.connect(self._enable_stim_widgets)
        self.cmbStimulus.currentIndexChanged.connect(self._enable_stim_widgets)

        self.cmbNoise.currentIndexChanged.connect(self._update_noi)
        self.ledNoi.editingFinished.connect(self._update_noi)
        self.ledAmp1.editingFinished.connect(self._update_amp1)
        self.ledAmp2.editingFinished.connect(self._update_amp2)
        self.ledPhi1.editingFinished.connect(self._update_phi1)
        self.ledPhi2.editingFinished.connect(self._update_phi2)
        self.cmbChirpMethod.currentIndexChanged.connect(self._update_chirp_method)
        self.ledDC.editingFinished.connect(self._update_DC)
        self.ledStimFormula.editingFinished.connect(self._update_stim_formula)

#------------------------------------------------------------------------------
    def eventFilter(self, source, event):
        """
        Filter all events generated by the monitored widgets. Source and type
        of all events generated by monitored objects are passed to this eventFilter,
        evaluated and passed on to the next hierarchy level.

        - When a QLineEdit widget gains input focus (``QEvent.FocusIn``), display
          the stored value from filter dict with full precision
        - When a key is pressed inside the text field, set the `spec_edited` flag
          to True.
        - When a QLineEdit widget loses input focus (``QEvent.FocusOut``), store
          current value normalized to f_S with full precision (only if
          ``spec_edited == True``) and display the stored value in selected format
        """

        def _store_entry(source):
            if self.spec_edited:
                if source.objectName() == "stimFreq1":
                   self.f1 = safe_eval(source.text(), self.f1 * fb.fil[0]['f_S'],
                                            return_type='float') / fb.fil[0]['f_S']
                   source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S'])))

                elif source.objectName() == "stimFreq2":
                   self.f2 = safe_eval(source.text(), self.f2 * fb.fil[0]['f_S'],
                                            return_type='float') / fb.fil[0]['f_S']
                   source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S'])))

                self.spec_edited = False # reset flag
                self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim'})

#        if isinstance(source, QLineEdit):
#        if source.objectName() in {"stimFreq1","stimFreq2"}:
        if event.type() in {QEvent.FocusIn,QEvent.KeyPress, QEvent.FocusOut}:
            if event.type() == QEvent.FocusIn:
                self.spec_edited = False
                self.load_fs()
            elif event.type() == QEvent.KeyPress:
                self.spec_edited = True # entry has been changed
                key = event.key()
                if key in {Qt.Key_Return, Qt.Key_Enter}:
                    _store_entry(source)
                elif key == Qt.Key_Escape: # revert changes
                    self.spec_edited = False
                    if source.objectName() == "stimFreq1":
                        source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S'])))
                    elif source.objectName() == "stimFreq2":
                        source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S'])))

            elif event.type() == QEvent.FocusOut:
                _store_entry(source)

        # Call base class method to continue normal event processing:
        return super(PlotImpz_UI, self).eventFilter(source, event)

#-------------------------------------------------------------
    def _show_stim_options(self):
        """
        Hide / show panel with stimulus options
        """
        self.wdg_ctrl_stim.setVisible(self.chk_stim_options.isChecked())


    def _enable_stim_widgets(self):
        """ Enable / disable widgets depending on the selected stimulus"""
        self.stim = qget_cmb_box(self.cmbStimulus, data=False)
        f1_en = self.stim in {"Cos","Sine","Chirp","PM / FM","AM","Formula","Rect","Saw","Triang","Comb"}
        f2_en = self.stim in {"Cos","Sine","Chirp","PM / FM","AM","Formula"}
        dc_en = self.stim not in {"Step", "StepErr"}

        self.chk_stim_bl.setVisible(self.stim in {"Triang", "Saw", "Rect"})

        self.lblAmp1.setVisible(self.stim != "None")
        self.ledAmp1.setVisible(self.stim != "None")
        self.chk_scale_impz_f.setVisible(self.stim == 'Impulse')
        self.chk_scale_impz_f.setEnabled((self.noi == 0 or self.cmbNoise.currentText() == 'None')\
                                         and self.DC == 0)

        self.cmbChirpMethod.setVisible(self.stim == 'Chirp')

        self.lblPhi1.setVisible(f1_en)
        self.ledPhi1.setVisible(f1_en)
        self.lblPhU1.setVisible(f1_en)
        self.lblFreq1.setVisible(f1_en)
        self.ledFreq1.setVisible(f1_en)
        self.lblFreqUnit1.setVisible(f1_en)

        self.lblFreq2.setVisible(f2_en)
        self.ledFreq2.setVisible(f2_en)
        self.lblFreqUnit2.setVisible(f2_en)
        self.lblAmp2.setVisible(f2_en and self.stim != "Chirp")
        self.ledAmp2.setVisible(f2_en and self.stim != "Chirp")
        self.lblPhi2.setVisible(f2_en and self.stim != "Chirp")
        self.ledPhi2.setVisible(f2_en and self.stim != "Chirp")
        self.lblPhU2.setVisible(f2_en and self.stim != "Chirp")

        self.lblDC.setVisible(dc_en)
        self.ledDC.setVisible(dc_en)

        self.lblStimFormula.setVisible(self.stim == "Formula")
        self.ledStimFormula.setVisible(self.stim == "Formula")

        self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim'})

#-------------------------------------------------------------
    def load_fs(self):
        """
        Reload sampling frequency from filter dictionary and transform
        the displayed frequency spec input fields according to the units
        setting (i.e. f_S). Spec entries are always stored normalized w.r.t. f_S
        in the dictionary; when f_S or the unit are changed, only the displayed values
        of the frequency entries are updated, not the dictionary!

        load_fs() is called during init and when the frequency unit or the
        sampling frequency have been changed.

        It should be called when sigSpecsChanged or sigFilterDesigned is emitted
        at another place, indicating that a reload is required.
        """

        # recalculate displayed freq spec values for (maybe) changed f_S
        if self.ledFreq1.hasFocus():
            # widget has focus, show full precision
            self.ledFreq1.setText(str(self.f1 * fb.fil[0]['f_S']))
        elif self.ledFreq2.hasFocus():
            # widget has focus, show full precision
            self.ledFreq2.setText(str(self.f2 * fb.fil[0]['f_S']))
        else:
            # widgets have no focus, round the display
            self.ledFreq1.setText(
                str(params['FMT'].format(self.f1 * fb.fil[0]['f_S'])))
            self.ledFreq2.setText(
                str(params['FMT'].format(self.f2 * fb.fil[0]['f_S'])))


    def _update_amp1(self):
        """ Update value for self.A1 from QLineEditWidget"""
        self.A1 = safe_eval(self.ledAmp1.text(), self.A1, return_type='cmplx')
        self.ledAmp1.setText(str(self.A1))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'a1'})

    def _update_amp2(self):
        """ Update value for self.A2 from the QLineEditWidget"""
        self.A2 = safe_eval(self.ledAmp2.text(), self.A2, return_type='cmplx')
        self.ledAmp2.setText(str(self.A2))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'a2'})

    def _update_phi1(self):
        """ Update value for self.phi1 from QLineEditWidget"""
        self.phi1 = safe_eval(self.ledPhi1.text(), self.phi1, return_type='float')
        self.ledPhi1.setText(str(self.phi1))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'phi1'})

    def _update_phi2(self):
        """ Update value for self.phi2 from the QLineEditWidget"""
        self.phi2 = safe_eval(self.ledPhi2.text(), self.phi2, return_type='float')
        self.ledPhi2.setText(str(self.phi2))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'phi2'})

    def _update_chirp_method(self):
        """ Update value for self.chirp_method from the QLineEditWidget"""
        self.chirp_method = qget_cmb_box(self.cmbChirpMethod) # read current data string
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'chirp_method'})


    def _update_noi(self):
        """ Update type + value + label for self.noi for noise"""
        self.noise = qget_cmb_box(self.cmbNoise, data=False).lower()
        self.lblNoi.setVisible(self.noise!='none')
        self.ledNoi.setVisible(self.noise!='none')
        if self.noise!='none':
            self.noi = safe_eval(self.ledNoi.text(), 0, return_type='cmplx')
            self.ledNoi.setText(str(self.noi))
            if self.noise == 'gauss':
                self.lblNoi.setText(to_html("&nbsp;&sigma; =", frmt='bi'))
                self.ledNoi.setToolTip("<span>Standard deviation of statistical process,"
                                       "noise power is <i>P</i> = &sigma;<sup>2</sup></span>")
            elif self.noise == 'uniform':
                self.lblNoi.setText(to_html("&nbsp;&Delta; =", frmt='bi'))
                self.ledNoi.setToolTip("<span>Interval size for uniformly distributed process "
                                       "(e.g. quantization step size for quantization noise), "
                                       "centered around 0. Noise power is "
                                       "<i>P</i> = &Delta;<sup>2</sup>/12.</span>")
            elif self.noise == 'prbs':
                self.lblNoi.setText(to_html("&nbsp;A =", frmt='bi'))
                self.ledNoi.setToolTip("<span>Amplitude of bipolar Pseudorandom Binary Sequence. "
                                       "Noise power is <i>P</i> = A<sup>2</sup>.</span>")

        self.sig_tx.emit({'sender':__name__, 'ui_changed':'noi'})

    def _update_DC(self):
        """ Update value for self.DC from the QLineEditWidget"""
        self.DC = safe_eval(self.ledDC.text(), 0, return_type='cmplx')
        self.ledDC.setText(str(self.DC))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'dc'})

    def _update_stim_formula(self):
        """Update string with formula to be evaluated by numexpr"""
        self.stim_formula = self.ledStimFormula.text().strip()
        self.ledStimFormula.setText(str(self.stim_formula))
        self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim_formula'})

    # -------------------------------------------------------------------------

    def update_N(self, emit=True):
        # called directly from impz or locally
        # between local triggering and updates upstream
        """
        Update values for self.N and self.N_start from the QLineEditWidget,
        update the window and fire "ui_changed"
        """
        if not isinstance(emit, bool):
            logger.error("update N: emit={0}".format(emit))
        self.N_start = safe_eval(self.led_N_start.text(), self.N_start, return_type='int', sign='poszero')
        self.led_N_start.setText(str(self.N_start)) # update widget
        self.N_user = safe_eval(self.led_N_points.text(), self.N_user, return_type='int', sign='poszero')

        if self.N_user == 0: # automatic calculation
            self.N = self.calc_n_points(self.N_user) # widget remains set to 0
            self.led_N_points.setText("0") # update widget
        else:
            self.N = self.N_user
            self.led_N_points.setText(str(self.N)) # update widget

        self.N_end = self.N + self.N_start # total number of points to be calculated: N + N_start

        # FFT window needs to be updated due to changed number of data points
        self._update_win_fft(emit=False) # don't emit anything here
        if emit:
            self.sig_tx.emit({'sender':__name__, 'ui_changed':'N'})


    def _read_param1(self):
        """Read out textbox when editing is finished and update dict and fft window"""
        param = safe_eval(self.ledWinPar1.text(), self.win_dict['par'][0]['val'],
                          return_type='float')
        if param < self.win_dict['par'][0]['min']:
            param = self.win_dict['par'][0]['min']
        elif param > self.win_dict['par'][0]['max']:
            param = self.win_dict['par'][0]['max']
        self.ledWinPar1.setText(str(param))
        self.win_dict['par'][0]['val'] = param
        self._update_win_fft()

    def _read_param2(self):
        """Read out textbox when editing is finished and update dict and fft window"""
        param = safe_eval(self.ledWinPar2.text(), self.win_dict['par'][1]['val'],
                          return_type='float')
        if param < self.win_dict['par'][1]['min']:
            param = self.win_dict['par'][1]['min']
        elif param > self.win_dict['par'][1]['max']:
            param = self.win_dict['par'][1]['max']
        self.ledWinPar2.setText(str(param))
        self.win_dict['par'][1]['val'] = param
        self._update_win_fft()

#------------------------------------------------------------------------------
    def _update_win_fft(self, arg=None, emit=True):
        """
        Update window type for FFT  with different arguments:

        - signal-slot connection to combo-box -> index (int), absorbed by `arg`
                                                 emit is not set -> emit=True
        - called by _read_param() -> empty -> emit=True
        - called by update_N(emit=False)

        """
        if not isinstance(emit, bool):
            logger.error("update win: emit={0}".format(emit))
        self.window_name = qget_cmb_box(self.cmb_win_fft, data=False)
        self.win = calc_window_function(self.win_dict, self.window_name,
                                        N=self.N, sym=False)

        n_par = self.win_dict['n_par']

        self.lblWinPar1.setVisible(n_par > 0)
        self.ledWinPar1.setVisible(n_par > 0)
        self.lblWinPar2.setVisible(n_par > 1)
        self.ledWinPar2.setVisible(n_par > 1)

        if n_par > 0:
            self.lblWinPar1.setText(to_html(self.win_dict['par'][0]['name'] + " =", frmt='bi'))
            self.ledWinPar1.setText(str(self.win_dict['par'][0]['val']))
            self.ledWinPar1.setToolTip(self.win_dict['par'][0]['tooltip'])

        if n_par > 1:
            self.lblWinPar2.setText(to_html(self.win_dict['par'][1]['name'] + " =", frmt='bi'))
            self.ledWinPar2.setText(str(self.win_dict['par'][1]['val']))
            self.ledWinPar2.setToolTip(self.win_dict['par'][1]['tooltip'])


        self.nenbw = self.N * np.sum(np.square(self.win)) / (np.square(np.sum(self.win)))

        self.cgain = np.sum(self.win) / self.N # coherent gain
        self.win /= self.cgain # correct gain for periodic signals

        # only emit a signal for local triggers to prevent infinite loop:
        # - signal-slot connection passes a bool or an integer
        # - local function calls don't pass anything
        if emit is True:
            self.sig_tx.emit({'sender':__name__, 'ui_changed':'win'})
        # ... but always notify the FFT widget via sig_tx_fft
        self.sig_tx_fft.emit({'sender':__name__, 'view_changed':'win'})

    #------------------------------------------------------------------------------
    def show_fft_win(self):
        """
        Pop-up FFT window
        """
        if self.but_fft_win.isChecked():
            qstyle_widget(self.but_fft_win, "changed")
        else:
            qstyle_widget(self.but_fft_win, "normal")

        if self.fft_window is None: # no handle to the window? Create a new instance
            if self.but_fft_win.isChecked():
                # Important: Handle to window must be class attribute otherwise it
                # (and the attached window) is deleted immediately when it goes out of scope
                self.fft_window = Plot_FFT_win(self, win_dict=self.win_dict, sym=False,
                                               title="pyFDA Spectral Window Viewer")
                self.sig_tx_fft.connect(self.fft_window.sig_rx)
                self.fft_window.sig_tx.connect(self.close_fft_win)
                self.fft_window.show() # modeless i.e. non-blocking popup window
        else:
            if not self.but_fft_win.isChecked():
                if self.fft_window is None:
                    logger.warning("FFT window is already closed!")
                else:
                    self.fft_window.close()

    def close_fft_win(self):
        self.fft_window = None
        self.but_fft_win.setChecked(False)
        qstyle_widget(self.but_fft_win, "normal")


#------------------------------------------------------------------------------
    def calc_n_points(self, N_user = 0):
        """
        Calculate number of points to be displayed, depending on type of filter
        (FIR, IIR) and user input. If the user selects 0 points, the number is
        calculated automatically.

        An improvement would be to calculate the dominant pole and the corresponding
        settling time.
        """
        if N_user == 0: # set number of data points automatically
            if fb.fil[0]['ft'] == 'IIR':
                N = 100
            else:
                N = min(len(fb.fil[0]['ba'][0]),100) # FIR: N = number of coefficients (max. 100)
        else:
            N = N_user

        return N
Exemplo n.º 6
0
class Plot_3D(QWidget):
    """
    Class for various 3D-plots:
    - lin / log line plot of H(f)
    - lin / log surf plot of H(z)
    - optional display of poles / zeros
    """

    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    #    sig_tx = pyqtSignal(object) # outgoing from process_signals

    def __init__(self):
        super().__init__()
        self.zmin = 0
        self.zmax = 4
        self.zmin_dB = -80
        self.cmap_default = 'RdYlBu'
        self.data_changed = True  # flag whether data has changed
        self.tool_tip = "3D magnitude response |H(z)|"
        self.tab_label = "3D"

        self._construct_UI()

# ------------------------------------------------------------------------------

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from ``sig_rx``
        """
        # logger.debug("Processing {0} | data_changed = {1}, visible = {2}"\
        #              .format(dict_sig, self.data_changed, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.data_changed:
                self.draw()
                self.data_changed = False
        else:
            if 'data_changed' in dict_sig:
                self.data_changed = True

# ------------------------------------------------------------------------------

    def _construct_UI(self):
        self.but_log = PushButton("dB", checked=False)
        self.but_log.setObjectName("but_log")
        self.but_log.setToolTip("Logarithmic scale")

        self.but_plot_in_UC = PushButton("|z| < 1 ", checked=False)
        self.but_plot_in_UC.setObjectName("but_plot_in_UC")
        self.but_plot_in_UC.setToolTip("Only plot H(z) within the unit circle")

        self.lblBottom = QLabel(to_html("Bottom =", frmt='bi'), self)
        self.ledBottom = QLineEdit(self)
        self.ledBottom.setObjectName("ledBottom")
        self.ledBottom.setText(str(self.zmin))
        self.ledBottom.setToolTip("Minimum display value.")
        self.lblBottomdB = QLabel("dB", self)
        self.lblBottomdB.setVisible(self.but_log.isChecked())

        self.lblTop = QLabel(to_html("Top =", frmt='bi'), self)
        self.ledTop = QLineEdit(self)
        self.ledTop.setObjectName("ledTop")
        self.ledTop.setText(str(self.zmax))
        self.ledTop.setToolTip("Maximum display value.")
        self.lblTopdB = QLabel("dB", self)
        self.lblTopdB.setVisible(self.but_log.isChecked())

        self.plt_UC = PushButton("UC", checked=True)
        self.plt_UC.setObjectName("plt_UC")
        self.plt_UC.setToolTip("Plot unit circle")

        self.but_PZ = PushButton("P/Z ", checked=True)
        self.but_PZ.setObjectName("but_PZ")
        self.but_PZ.setToolTip("Plot poles and zeros")

        self.but_Hf = PushButton("H(f) ", checked=True)
        self.but_Hf.setObjectName("but_Hf")
        self.but_Hf.setToolTip("Plot H(f) along the unit circle")

        modes = ['None', 'Mesh', 'Surf', 'Contour']
        self.cmbMode3D = QComboBox(self)
        self.cmbMode3D.addItems(modes)
        self.cmbMode3D.setObjectName("cmbShow3D")
        self.cmbMode3D.setToolTip("Select 3D-plot mode.")
        self.cmbMode3D.setCurrentIndex(0)
        self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.but_colormap_r = PushButton("reverse", checked=True)
        self.but_colormap_r.setObjectName("but_colormap_r")
        self.but_colormap_r.setToolTip("reverse colormap")

        self.cmbColormap = QComboBox(self)
        self._init_cmb_colormap(cmap_init=self.cmap_default)
        self.cmbColormap.setToolTip("Select colormap")

        self.but_colbar = PushButton("Colorbar ", checked=False)
        self.but_colbar.setObjectName("chkColBar")
        self.but_colbar.setToolTip("Show colorbar")

        self.but_lighting = PushButton("Lighting", checked=False)
        self.but_lighting.setObjectName("but_lighting")
        self.but_lighting.setToolTip("Enable light source")

        self.lblAlpha = QLabel(to_html("Alpha", frmt='bi'), self)
        self.diaAlpha = QDial(self)
        self.diaAlpha.setRange(0, 10)
        self.diaAlpha.setValue(10)
        self.diaAlpha.setTracking(False)  # produce less events when turning
        self.diaAlpha.setFixedHeight(30)
        self.diaAlpha.setFixedWidth(30)
        self.diaAlpha.setWrapping(False)
        self.diaAlpha.setToolTip(
            "<span>Set transparency for surf and contour plots.</span>")

        self.lblHatch = QLabel(to_html("Stride", frmt='bi'), self)
        self.diaHatch = QDial(self)
        self.diaHatch.setRange(0, 9)
        self.diaHatch.setValue(5)
        self.diaHatch.setTracking(False)  # produce less events when turning
        self.diaHatch.setFixedHeight(30)
        self.diaHatch.setFixedWidth(30)
        self.diaHatch.setWrapping(False)
        self.diaHatch.setToolTip("Set line density for various plots.")

        self.but_contour_2d = PushButton("Contour2D ", checked=False)
        self.but_contour_2d.setObjectName("chkContour2D")
        self.but_contour_2d.setToolTip("Plot 2D-contours at z =0")

        # ----------------------------------------------------------------------
        # LAYOUT for UI widgets
        # ----------------------------------------------------------------------
        layGControls = QGridLayout()
        layGControls.addWidget(self.but_log, 0, 0)
        layGControls.addWidget(self.but_plot_in_UC, 1, 0)
        layGControls.addWidget(self.lblTop, 0, 2)
        layGControls.addWidget(self.ledTop, 0, 4)
        layGControls.addWidget(self.lblTopdB, 0, 5)
        layGControls.addWidget(self.lblBottom, 1, 2)
        layGControls.addWidget(self.ledBottom, 1, 4)
        layGControls.addWidget(self.lblBottomdB, 1, 5)
        layGControls.setColumnStretch(5, 1)

        layGControls.addWidget(self.plt_UC, 0, 6)
        layGControls.addWidget(self.but_Hf, 1, 6)
        layGControls.addWidget(self.but_PZ, 0, 8)

        layGControls.addWidget(self.cmbMode3D, 0, 10)
        layGControls.addWidget(self.but_contour_2d, 1, 10)
        layGControls.addWidget(self.cmbColormap, 0, 12, 1, 1)
        layGControls.addWidget(self.but_colormap_r, 1, 12)

        layGControls.addWidget(self.but_lighting, 0, 14)
        layGControls.addWidget(self.but_colbar, 1, 14)

        layGControls.addWidget(self.lblAlpha, 0, 15)
        layGControls.addWidget(self.diaAlpha, 0, 16)

        layGControls.addWidget(self.lblHatch, 1, 15)
        layGControls.addWidget(self.diaHatch, 1, 16)

        # This widget encompasses all control subwidgets
        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layGControls)

        # ----------------------------------------------------------------------
        # mplwidget
        # ----------------------------------------------------------------------
        # This is the plot pane widget, encompassing the other widgets
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins'])
        self.mplwidget.mplToolbar.a_he.setEnabled(True)
        self.mplwidget.mplToolbar.a_he.info = "manual/plot_3d.html"
        self.setLayout(self.mplwidget.layVMainMpl)

        self._init_grid()  # initialize grid and do initial plot

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.but_log.clicked.connect(self._log_clicked)
        self.ledBottom.editingFinished.connect(self._log_clicked)
        self.ledTop.editingFinished.connect(self._log_clicked)

        self.but_plot_in_UC.clicked.connect(self._init_grid)
        self.plt_UC.clicked.connect(self.draw)
        self.but_Hf.clicked.connect(self.draw)
        self.but_PZ.clicked.connect(self.draw)
        self.cmbMode3D.currentIndexChanged.connect(self.draw)
        self.but_colbar.clicked.connect(self.draw)

        self.cmbColormap.currentIndexChanged.connect(self.draw)
        self.but_colormap_r.clicked.connect(self.draw)

        self.but_lighting.clicked.connect(self.draw)
        self.diaAlpha.valueChanged.connect(self.draw)
        self.diaHatch.valueChanged.connect(self.draw)
        self.but_contour_2d.clicked.connect(self.draw)

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        # self.mplwidget.mplToolbar.enable_plot(state = False) # disable initially

# ------------------------------------------------------------------------------

    def _init_cmb_colormap(self, cmap_init):
        """
        Initialize combobox with available colormaps and try to set it to `cmap_init`

        Since matplotlib 3.2 the reversed "*_r" colormaps are no longer contained in
        `cm.datad`. They are now obtained by using the `reversed()` method (much simpler!)

        `cm.datad` doesn't return the "new" colormaps like viridis, instead the
        `colormaps()` method is used.
        """
        self.cmbColormap.addItems(
            [m for m in colormaps() if not m.endswith("_r")])

        idx = self.cmbColormap.findText(cmap_init)
        if idx == -1:
            idx = 0
        self.cmbColormap.setCurrentIndex(idx)

# ------------------------------------------------------------------------------

    def _init_grid(self):
        """ Initialize (x,y,z) coordinate grid + (re)draw plot."""
        phi_UC = np.linspace(0, 2 * pi, 400,
                             endpoint=True)  # angles for unit circle
        self.xy_UC = np.exp(1j * phi_UC)  # x,y coordinates of unity circle

        steps = 100  # number of steps for x, y, r, phi
        # cartesian range limits
        self.xmin = -1.5
        self.xmax = 1.5
        self.ymin = -1.5
        self.ymax = 1.5

        # Polar range limits
        rmin = 0
        rmax = 1

        # Calculate grids for 3D-Plots
        dr = rmax / steps * 2  # grid size for polar range
        dx = (self.xmax - self.xmin) / steps
        dy = (self.ymax - self.ymin) / steps  # grid size cartesian range

        if self.but_plot_in_UC.isChecked():  # Plot circular range in 3D-Plot
            [r,
             phi] = np.meshgrid(np.arange(rmin, rmax, dr),
                                np.linspace(0, 2 * pi, steps, endpoint=True))
            self.x = r * cos(phi)
            self.y = r * sin(phi)
        else:  # cartesian grid
            [self.x, self.y] = np.meshgrid(np.arange(self.xmin, self.xmax, dx),
                                           np.arange(self.ymin, self.ymax, dy))

        self.z = self.x + 1j * self.y  # create coordinate grid for complex plane

        self.draw()  # initial plot

# ------------------------------------------------------------------------------

    def init_axes(self):
        """
        Initialize and clear the axes to get rid of colorbar
        The azimuth / elevation / distance settings of the camera are restored
        after clearing the axes. See
        http://stackoverflow.com/questions/4575588/matplotlib-3d-plot-with-pyqt4-in-qtabwidget-mplwidget
        """

        self._save_axes()

        self.mplwidget.fig.clf()  # needed to get rid of colorbar
        self.ax3d = self.mplwidget.fig.add_subplot(111, projection='3d')
        # self.ax3d = self.mplwidget.fig.subplots(nrows=1, ncols=1, projection='3d')

        self._restore_axes()

# ------------------------------------------------------------------------------

    def _save_axes(self):
        """
        Store x/y/z - limits and camera position
        """

        try:
            self.azim = self.ax3d.azim
            self.elev = self.ax3d.elev
            self.dist = self.ax3d.dist
            self.xlim = self.ax3d.get_xlim3d()
            self.ylim = self.ax3d.get_ylim3d()
            self.zlim = self.ax3d.get_zlim3d()

        except AttributeError:  # not yet initialized, set standard values
            self.azim = -65
            self.elev = 30
            self.dist = 10
            self.xlim = (self.xmin, self.xmax)
            self.ylim = (self.ymin, self.ymax)
            self.zlim = (self.zmin, self.zmax)

# ------------------------------------------------------------------------------

    def _restore_axes(self):
        """
        Restore x/y/z - limits and camera position
        """
        if self.mplwidget.mplToolbar.a_lk.isChecked():
            self.ax3d.set_xlim3d(self.xlim)
            self.ax3d.set_ylim3d(self.ylim)
            self.ax3d.set_zlim3d(self.zlim)
        self.ax3d.azim = self.azim
        self.ax3d.elev = self.elev
        self.ax3d.dist = self.dist

# ------------------------------------------------------------------------------

    def _log_clicked(self):
        """
        Change scale and settings to log / lin when log setting is changed
        Update min / max settings when lineEdits have been edited
        """
        if self.sender().objectName(
        ) == 'but_log':  # clicking but_log triggered the slot
            if self.but_log.isChecked():
                self.ledBottom.setText(str(self.zmin_dB))
                self.zmax_dB = np.round(20 * log10(self.zmax), 2)
                self.ledTop.setText(str(self.zmax_dB))
                self.lblTopdB.setVisible(True)
                self.lblBottomdB.setVisible(True)
            else:
                self.ledBottom.setText(str(self.zmin))
                self.zmax = np.round(10**(self.zmax_dB / 20), 2)
                self.ledTop.setText(str(self.zmax))
                self.lblTopdB.setVisible(False)
                self.lblBottomdB.setVisible(False)

        else:  # finishing a lineEdit field triggered the slot
            if self.but_log.isChecked():
                self.zmin_dB = safe_eval(self.ledBottom.text(),
                                         self.zmin_dB,
                                         return_type='float')
                self.ledBottom.setText(str(self.zmin_dB))
                self.zmax_dB = safe_eval(self.ledTop.text(),
                                         self.zmax_dB,
                                         return_type='float')
                self.ledTop.setText(str(self.zmax_dB))
            else:
                self.zmin = safe_eval(self.ledBottom.text(),
                                      self.zmin,
                                      return_type='float')
                self.ledBottom.setText(str(self.zmin))
                self.zmax = safe_eval(self.ledTop.text(),
                                      self.zmax,
                                      return_type='float')
                self.ledTop.setText(str(self.zmax))

        self.draw()

# ------------------------------------------------------------------------------

    def draw(self):
        """
        Main drawing entry point: perform the actual plot
        """
        self.draw_3d()

# ------------------------------------------------------------------------------

    def draw_3d(self):
        """
        Draw various 3D plots
        """
        self.init_axes()

        bb = fb.fil[0]['ba'][0]
        aa = fb.fil[0]['ba'][1]

        zz = np.array(fb.fil[0]['zpk'][0])
        pp = np.array(fb.fil[0]['zpk'][1])

        wholeF = fb.fil[0]['freqSpecsRangeType'] != 'half'  # not used
        f_S = fb.fil[0]['f_S']
        N_FFT = params['N_FFT']

        alpha = self.diaAlpha.value() / 10.

        cmap = cm.get_cmap(str(self.cmbColormap.currentText()))
        if self.but_colormap_r.isChecked():
            cmap = cmap.reversed()  # use reversed colormap

        # Number of Lines /step size for H(f) stride, mesh, contour3d:
        stride = 10 - self.diaHatch.value()
        NL = 3 * self.diaHatch.value() + 5

        surf_enabled = qget_cmb_box(self.cmbMode3D, data=False) in {'Surf', 'Contour'}\
            or self.but_contour_2d.isChecked()
        self.cmbColormap.setEnabled(surf_enabled)
        self.but_colormap_r.setEnabled(surf_enabled)
        self.but_lighting.setEnabled(surf_enabled)
        self.but_colbar.setEnabled(surf_enabled)
        self.diaAlpha.setEnabled(surf_enabled
                                 or self.but_contour_2d.isChecked())

        # cNorm  = colors.Normalize(vmin=0, vmax=values[-1])
        # scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet)

        # -----------------------------------------------------------------------------
        # Calculate H(w) along the upper half of unity circle
        # -----------------------------------------------------------------------------

        [w, H] = sig.freqz(bb, aa, worN=N_FFT, whole=True)
        H = np.nan_to_num(H)  # replace nans and inf by finite numbers

        H_abs = abs(H)
        H_max = max(H_abs)
        H_min = min(H_abs)
        # f = w / (2 * pi) * f_S                  # translate w to absolute frequencies
        # F_min = f[np.argmin(H_abs)]

        plevel_rel = 1.05  # height of plotted pole position relative to zmax
        zlevel_rel = 0.1  # height of plotted zero position relative to zmax

        if self.but_log.isChecked():  # logarithmic scale
            # suppress "divide by zero in log10" warnings
            old_settings_seterr = np.seterr()
            np.seterr(divide='ignore')

            bottom = np.floor(max(self.zmin_dB, 20 * log10(H_min)) / 10) * 10
            top = self.zmax_dB
            top_bottom = top - bottom

            zlevel = bottom - top_bottom * zlevel_rel

            if self.cmbMode3D.currentText(
            ) == 'None':  # "Poleposition": H(f) plot only
                plevel_top = 2 * bottom - zlevel  # height of displayed pole position
                plevel_btm = bottom
            else:
                plevel_top = top + top_bottom * (plevel_rel - 1)
                plevel_btm = top

            np.seterr(**old_settings_seterr)

        else:  # linear scale
            bottom = max(self.zmin, H_min)  # min. display value
            top = self.zmax  # max. display value
            top_bottom = top - bottom
            #   top = zmax_rel * H_max # calculate display top from max. of H(f)

            zlevel = bottom + top_bottom * zlevel_rel  # height of displayed zero position

            if self.cmbMode3D.currentText(
            ) == 'None':  # "Poleposition": H(f) plot only
                #H_max = np.clip(max(H_abs), 0, self.zmax)
                # make height of displayed poles same to zeros
                plevel_top = bottom + top_bottom * zlevel_rel
                plevel_btm = bottom
            else:
                plevel_top = plevel_rel * top
                plevel_btm = top

        # calculate H(jw)| along the unity circle and |H(z)|, each clipped
        # between bottom and top
        H_UC = H_mag(bb,
                     aa,
                     self.xy_UC,
                     top,
                     H_min=bottom,
                     log=self.but_log.isChecked())
        Hmag = H_mag(bb,
                     aa,
                     self.z,
                     top,
                     H_min=bottom,
                     log=self.but_log.isChecked())

        # ===============================================================
        # Plot Unit Circle (UC)
        # ===============================================================
        if self.plt_UC.isChecked():
            #  Plot unit circle and marker at (1,0):
            self.ax3d.plot(self.xy_UC.real,
                           self.xy_UC.imag,
                           ones(len(self.xy_UC)) * bottom,
                           lw=2,
                           color='k')
            self.ax3d.plot([0.97, 1.03], [0, 0], [bottom, bottom],
                           lw=2,
                           color='k')

        # ===============================================================
        # Plot ||H(f)| along unit circle as 3D-lineplot
        # ===============================================================
        if self.but_Hf.isChecked():
            self.ax3d.plot(self.xy_UC.real,
                           self.xy_UC.imag,
                           H_UC,
                           alpha=0.8,
                           lw=4)
            # draw once more as dashed white line to improve visibility
            self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, H_UC, 'w--', lw=4)

            if stride < 10:  # plot thin vertical line every stride points on the UC
                for k in range(len(self.xy_UC[::stride])):
                    self.ax3d.plot([
                        self.xy_UC.real[::stride][k],
                        self.xy_UC.real[::stride][k]
                    ], [
                        self.xy_UC.imag[::stride][k],
                        self.xy_UC.imag[::stride][k]
                    ], [
                        np.ones(len(self.xy_UC[::stride]))[k] * bottom,
                        H_UC[::stride][k]
                    ],
                                   linewidth=1,
                                   color=(0.5, 0.5, 0.5))

        # ===============================================================
        # Plot Poles and Zeros
        # ===============================================================
        if self.but_PZ.isChecked():

            PN_SIZE = 8  # size of P/N symbols

            # Plot zero markers at |H(z_i)| = zlevel with "stems":
            self.ax3d.plot(zz.real,
                           zz.imag,
                           ones(len(zz)) * zlevel,
                           'o',
                           markersize=PN_SIZE,
                           markeredgecolor='blue',
                           markeredgewidth=2.0,
                           markerfacecolor='none')
            for k in range(len(zz)):  # plot zero "stems"
                self.ax3d.plot([zz[k].real, zz[k].real],
                               [zz[k].imag, zz[k].imag], [bottom, zlevel],
                               linewidth=1,
                               color='b')

            # Plot the poles at |H(z_p)| = plevel with "stems":
            self.ax3d.plot(np.real(pp),
                           np.imag(pp),
                           plevel_top,
                           'x',
                           markersize=PN_SIZE,
                           markeredgewidth=2.0,
                           markeredgecolor='red')
            for k in range(len(pp)):  # plot pole "stems"
                self.ax3d.plot([pp[k].real, pp[k].real],
                               [pp[k].imag, pp[k].imag],
                               [plevel_btm, plevel_top],
                               linewidth=1,
                               color='r')

        # ===============================================================
        # 3D-Plots of |H(z)| clipped between |H(z)| = top
        # ===============================================================

        m_cb = cm.ScalarMappable(
            cmap=cmap)  # normalized proxy object that is mappable
        m_cb.set_array(Hmag)  # for colorbar

        # ---------------------------------------------------------------
        # 3D-mesh plot
        # ---------------------------------------------------------------
        if self.cmbMode3D.currentText() == 'Mesh':
            # fig_mlab = mlab.figure(fgcolor=(0., 0., 0.), bgcolor=(1, 1, 1))
            # self.ax3d.set_zlim(0,2)
            self.ax3d.plot_wireframe(self.x,
                                     self.y,
                                     Hmag,
                                     rstride=5,
                                     cstride=stride,
                                     linewidth=1,
                                     color='gray')

        # ---------------------------------------------------------------
        # 3D-surface plot
        # ---------------------------------------------------------------
        # http://stackoverflow.com/questions/28232879/phong-shading-for-shiny-python-3d-surface-plots
        elif self.cmbMode3D.currentText() == 'Surf':
            if MLAB:
                # Mayavi
                surf = mlab.surf(self.x,
                                 self.y,
                                 H_mag,
                                 colormap='RdYlBu',
                                 warp_scale='auto')
                # Change the visualization parameters.
                surf.actor.property.interpolation = 'phong'
                surf.actor.property.specular = 0.1
                surf.actor.property.specular_power = 5
                #                s = mlab.contour_surf(self.x, self.y, Hmag, contour_z=0)
                mlab.show()

            else:
                if self.but_lighting.isChecked():
                    ls = LightSource(azdeg=0,
                                     altdeg=65)  # Create light source object
                    rgb = ls.shade(
                        Hmag, cmap=cmap)  # Shade data, creating an rgb array
                    cmap_surf = None
                else:
                    rgb = None
                    cmap_surf = cmap

    #            s = self.ax3d.plot_surface(self.x, self.y, Hmag,
    #                    alpha=OPT_3D_ALPHA, rstride=1, cstride=1, cmap=cmap,
    #                    linewidth=0, antialiased=False, shade=True, facecolors = rgb)
    #            s.set_edgecolor('gray')
                s = self.ax3d.plot_surface(self.x,
                                           self.y,
                                           Hmag,
                                           alpha=alpha,
                                           rstride=1,
                                           cstride=1,
                                           linewidth=0,
                                           antialiased=False,
                                           facecolors=rgb,
                                           cmap=cmap_surf,
                                           shade=True)
                s.set_edgecolor(None)
        # ---------------------------------------------------------------
        # 3D-Contour plot
        # ---------------------------------------------------------------
        elif self.cmbMode3D.currentText() == 'Contour':
            s = self.ax3d.contourf3D(self.x,
                                     self.y,
                                     Hmag,
                                     NL,
                                     alpha=alpha,
                                     cmap=cmap)

        # ---------------------------------------------------------------
        # 2D-Contour plot
        # TODO: 2D contour plots do not plot correctly together with 3D plots in
        #       current matplotlib 1.4.3 -> disable them for now
        # TODO: zdir = x / y delivers unexpected results -> rather plot max(H)
        #       along the other axis?
        # TODO: colormap is created depending on the zdir = 'z' contour plot
        #       -> set limits of (all) other plots manually?
        if self.but_contour_2d.isChecked():
            #            self.ax3d.contourf(x, y, Hmag, 20, zdir='x', offset=xmin,
            #                         cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom)
            #            self.ax3d.contourf(x, y, Hmag, 20, zdir='y', offset=ymax,
            #                         cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom)
            s = self.ax3d.contourf(self.x,
                                   self.y,
                                   Hmag,
                                   NL,
                                   zdir='z',
                                   offset=bottom - (top - bottom) * 0.05,
                                   cmap=cmap,
                                   alpha=alpha)

        # plot colorbar for suitable plot modes
        if self.but_colbar.isChecked() and (
                self.but_contour_2d.isChecked()
                or str(self.cmbMode3D.currentText()) in {'Contour', 'Surf'}):
            self.colb = self.mplwidget.fig.colorbar(m_cb,
                                                    ax=self.ax3d,
                                                    shrink=0.8,
                                                    aspect=20,
                                                    pad=0.02,
                                                    fraction=0.08)

        # ----------------------------------------------------------------------
        # Set view limits and labels
        # ----------------------------------------------------------------------
        if not self.mplwidget.mplToolbar.a_lk.isChecked():
            self.ax3d.set_xlim3d(self.xmin, self.xmax)
            self.ax3d.set_ylim3d(self.ymin, self.ymax)
            self.ax3d.set_zlim3d(bottom, top)
        else:
            self._restore_axes()

        self.ax3d.set_xlabel('Re')  #(fb.fil[0]['plt_fLabel'])
        self.ax3d.set_ylabel(
            'Im'
        )  #(r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $')
        #        self.ax3d.set_zlabel(r'$|H(z)|\; \rightarrow $')
        self.ax3d.set_title(
            r'3D-Plot of $|H(\mathrm{e}^{\mathrm{j} \Omega})|$ and $|H(z)|$')

        self.redraw()

# ------------------------------------------------------------------------------

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 7
0
class Firwin(QWidget):

    FRMT = 'ba'  # output format(s) of filter design routines 'zpk' / 'ba' / 'sos'
    # currently, only 'ba' is supported for firwin routines

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.ft = 'FIR'
        self.fft_window = None
        # dictionary for firwin window settings
        self.win_dict = fb.fil[0]['win_fir']

        c = Common()
        self.rt_dict = c.rt_base_iir

        self.rt_dict_add = {
            'COM': {
                'min': {
                    'msg':
                    ('a',
                     r"<br /><b>Note:</b> Filter order is only a rough approximation "
                     "and most likely far too low!")
                },
                'man': {
                    'msg':
                    ('a', r"Enter desired filter order <b><i>N</i></b> and "
                     "<b>-6 dB</b> pass band corner "
                     "frequency(ies) <b><i>F<sub>C</sub></i></b> .")
                },
            },
            'LP': {
                'man': {},
                'min': {}
            },
            'HP': {
                'man': {
                    'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!")
                },
                'min': {}
            },
            'BS': {
                'man': {
                    'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!")
                },
                'min': {}
            },
            'BP': {
                'man': {},
                'min': {}
            },
        }

        self.info = """**Windowed FIR filters**
        
        are designed by truncating the
        infinite impulse response of an ideal filter with a window function.
        The kind of used window has strong influence on ripple etc. of the
        resulting filter.
        
        **Design routines:**

        ``scipy.signal.firwin()``

        """
        #self.info_doc = [] is set in self._update_UI()

        #------------------- end of static info for filter tree ---------------

        #----------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in 
        select_filter.py using the handle to the filter object, fb.filObj .
        """

        # Combobox for selecting the algorithm to estimate minimum filter order
        self.cmb_firwin_alg = QComboBox(self)
        self.cmb_firwin_alg.setObjectName('wdg_cmb_firwin_alg')
        self.cmb_firwin_alg.addItems(['ichige', 'kaiser', 'herrmann'])
        # Minimum size, can be changed in the upper hierarchy levels using layouts:
        self.cmb_firwin_alg.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmb_firwin_alg.hide()

        # Combobox for selecting the window used for filter design
        self.cmb_firwin_win = QComboBox(self)
        self.cmb_firwin_win.addItems(get_window_names())
        self.cmb_firwin_win.setObjectName('wdg_cmb_firwin_win')

        # Minimum size, can be changed in the upper hierarchy levels using layouts:
        self.cmb_firwin_win.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.but_fft_win = QPushButton(self)
        self.but_fft_win.setText("WIN FFT")
        self.but_fft_win.setToolTip(
            "Show time and frequency response of FFT Window")
        self.but_fft_win.setCheckable(True)
        self.but_fft_win.setChecked(False)

        self.lblWinPar1 = QLabel("a", self)
        self.lblWinPar1.setObjectName('wdg_lbl_firwin_1')
        self.ledWinPar1 = QLineEdit(self)
        self.ledWinPar1.setText("0.5")
        self.ledWinPar1.setObjectName('wdg_led_firwin_1')
        self.lblWinPar1.setVisible(False)
        self.ledWinPar1.setVisible(False)

        self.lblWinPar2 = QLabel("b", self)
        self.lblWinPar2.setObjectName('wdg_lbl_firwin_2')
        self.ledWinPar2 = QLineEdit(self)
        self.ledWinPar2.setText("0.5")
        self.ledWinPar2.setObjectName('wdg_led_firwin_2')
        self.ledWinPar2.setVisible(False)
        self.lblWinPar2.setVisible(False)

        self.layHWin1 = QHBoxLayout()
        self.layHWin1.addWidget(self.cmb_firwin_win)
        self.layHWin1.addWidget(self.but_fft_win)
        self.layHWin1.addWidget(self.cmb_firwin_alg)
        self.layHWin2 = QHBoxLayout()
        self.layHWin2.addWidget(self.lblWinPar1)
        self.layHWin2.addWidget(self.ledWinPar1)
        self.layHWin2.addWidget(self.lblWinPar2)
        self.layHWin2.addWidget(self.ledWinPar2)

        self.layVWin = QVBoxLayout()
        self.layVWin.addLayout(self.layHWin1)
        self.layVWin.addLayout(self.layHWin2)
        self.layVWin.setContentsMargins(0, 0, 0, 0)

        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layVWin)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.cmb_firwin_alg.activated.connect(self._update_win_fft)
        self.cmb_firwin_win.activated.connect(self._update_win_fft)
        self.ledWinPar1.editingFinished.connect(self._read_param1)
        self.ledWinPar2.editingFinished.connect(self._read_param2)

        self.but_fft_win.clicked.connect(self.show_fft_win)
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_win_fft()

#=============================================================================
# Copied from impz()
#==============================================================================

    def _read_param1(self):
        """Read out textbox when editing is finished and update dict and fft window"""
        param = safe_eval(self.ledWinPar1.text(),
                          self.win_dict['par'][0]['val'],
                          sign='pos',
                          return_type='float')
        if param < self.win_dict['par'][0]['min']:
            param = self.win_dict['par'][0]['min']
        elif param > self.win_dict['par'][0]['max']:
            param = self.win_dict['par'][0]['max']
        self.ledWinPar1.setText(str(param))
        self.win_dict['par'][0]['val'] = param
        self._update_win_fft()

    def _read_param2(self):
        """Read out textbox when editing is finished and update dict and fft window"""
        param = safe_eval(self.ledWinPar2.text(),
                          self.win_dict['par'][1]['val'],
                          return_type='float')
        if param < self.win_dict['par'][1]['min']:
            param = self.win_dict['par'][1]['min']
        elif param > self.win_dict['par'][1]['max']:
            param = self.win_dict['par'][1]['max']
        self.ledWinPar2.setText(str(param))
        self.win_dict['par'][1]['val'] = param
        self._update_win_fft()

    def _update_win_fft(self):
        """ Update window type for FirWin """
        self.alg = str(self.cmb_firwin_alg.currentText())
        self.fir_window_name = qget_cmb_box(self.cmb_firwin_win, data=False)
        self.win = calc_window_function(self.win_dict,
                                        self.fir_window_name,
                                        N=self.N,
                                        sym=True)
        n_par = self.win_dict['n_par']

        self.lblWinPar1.setVisible(n_par > 0)
        self.ledWinPar1.setVisible(n_par > 0)
        self.lblWinPar2.setVisible(n_par > 1)
        self.ledWinPar2.setVisible(n_par > 1)

        if n_par > 0:
            self.lblWinPar1.setText(
                to_html(self.win_dict['par'][0]['name'] + " =", frmt='bi'))
            self.ledWinPar1.setText(str(self.win_dict['par'][0]['val']))
            self.ledWinPar1.setToolTip(self.win_dict['par'][0]['tooltip'])

        if n_par > 1:
            self.lblWinPar2.setText(
                to_html(self.win_dict['par'][1]['name'] + " =", frmt='bi'))
            self.ledWinPar2.setText(str(self.win_dict['par'][1]['val']))
            self.ledWinPar2.setToolTip(self.win_dict['par'][1]['tooltip'])

        # sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'firwin'})

#=============================================================================

    def _load_dict(self):
        """
        Reload window selection and parameters from filter dictionary
        and set UI elements accordingly. load_dict() is called upon 
        initialization and when the filter is loaded from disk.
        """
        self.N = fb.fil[0]['N']
        win_idx = 0
        alg_idx = 0
        if 'wdg_fil' in fb.fil[0] and 'firwin' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['firwin']

            if 'win' in wdg_fil_par:
                if np.isscalar(
                        wdg_fil_par['win']):  # true for strings (non-vectors)
                    window = wdg_fil_par['win']
                else:
                    window = wdg_fil_par['win'][0]
                    self.ledWinPar1.setText(str(wdg_fil_par['win'][1]))
                    if len(wdg_fil_par['win']) > 2:
                        self.ledWinPar2.setText(str(wdg_fil_par['win'][2]))

                # find index for window string
                win_idx = self.cmb_firwin_win.findText(
                    window, Qt.MatchFixedString)  # case insensitive flag
                if win_idx == -1:  # Key does not exist, use first entry instead
                    win_idx = 0

            if 'alg' in wdg_fil_par:
                alg_idx = self.cmb_firwin_alg.findText(wdg_fil_par['alg'],
                                                       Qt.MatchFixedString)
                if alg_idx == -1:  # Key does not exist, use first entry instead
                    alg_idx = 0

        self.cmb_firwin_win.setCurrentIndex(
            win_idx)  # set index for window and
        self.cmb_firwin_alg.setCurrentIndex(alg_idx)  # and algorithm cmbBox

    def _store_entries(self):
        """
        Store window and alg. selection and parameter settings (part of 
        self.firWindow, if any) in filter dictionary.
        """
        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'firwin': {
                'win': self.firWindow,
                'alg': self.alg
            }})

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N = fil_dict['N']
        self.F_PB = fil_dict['F_PB']
        self.F_SB = fil_dict['F_SB']
        self.F_PB2 = fil_dict['F_PB2']
        self.F_SB2 = fil_dict['F_SB2']
        self.F_C = fil_dict['F_C']
        self.F_C2 = fil_dict['F_C2']

        # firwin amplitude specs are linear (not in dBs)
        self.A_PB = fil_dict['A_PB']
        self.A_PB2 = fil_dict['A_PB2']
        self.A_SB = fil_dict['A_SB']
        self.A_SB2 = fil_dict['A_SB2']

#        self.alg = 'ichige' # algorithm for determining the minimum order
#        self.alg = self.cmb_firwin_alg.currentText()

    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 1000:
            return qfilter_warning(self, self.N, "FirWin")
        else:
            return True

    def _save(self, fil_dict, arg):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        fil_save(fil_dict, arg, self.FRMT, __name__)

        try:  # has the order been calculated by a "min" filter design?
            fil_dict['N'] = self.N  # yes, update filterbroker
        except AttributeError:
            pass
#        self._store_entries()

#------------------------------------------------------------------------------

    def firwin(self,
               numtaps,
               cutoff,
               window=None,
               pass_zero=True,
               scale=True,
               nyq=1.0,
               fs=None):
        """
        FIR filter design using the window method. This is more or less the 
        same as `scipy.signal.firwin` with the exception that an ndarray with 
        the window values can be passed as an alternative to the window name.
        
        The parameters "width" (specifying a Kaiser window) and "fs" have been
        omitted, they are not needed here.

        This function computes the coefficients of a finite impulse response
        filter.  The filter will have linear phase; it will be Type I if
        `numtaps` is odd and Type II if `numtaps` is even.
        Type II filters always have zero response at the Nyquist rate, so a
        ValueError exception is raised if firwin is called with `numtaps` even and
        having a passband whose right end is at the Nyquist rate.
        
        Parameters
        ----------
        numtaps : int
            Length of the filter (number of coefficients, i.e. the filter
            order + 1).  `numtaps` must be even if a passband includes the
            Nyquist frequency.
        cutoff : float or 1D array_like
            Cutoff frequency of filter (expressed in the same units as `nyq`)
            OR an array of cutoff frequencies (that is, band edges). In the
            latter case, the frequencies in `cutoff` should be positive and
            monotonically increasing between 0 and `nyq`.  The values 0 and
            `nyq` must not be included in `cutoff`.
        window : ndarray or string
            string: use the window with the passed name from scipy.signal.windows
            
            ndarray: The window values - this is an addition to the original 
            firwin routine.
        pass_zero : bool, optional
            If True, the gain at the frequency 0 (i.e. the "DC gain") is 1.
            Otherwise the DC gain is 0.
        scale : bool, optional
            Set to True to scale the coefficients so that the frequency
            response is exactly unity at a certain frequency.
            That frequency is either:
            - 0 (DC) if the first passband starts at 0 (i.e. pass_zero
              is True)
            - `nyq` (the Nyquist rate) if the first passband ends at
              `nyq` (i.e the filter is a single band highpass filter);
              center of first passband otherwise
        nyq : float, optional
            Nyquist frequency.  Each frequency in `cutoff` must be between 0
            and `nyq`.
        Returns
        -------
        h : (numtaps,) ndarray
            Coefficients of length `numtaps` FIR filter.
        Raises
        ------
        ValueError
            If any value in `cutoff` is less than or equal to 0 or greater
            than or equal to `nyq`, if the values in `cutoff` are not strictly
            monotonically increasing, or if `numtaps` is even but a passband
            includes the Nyquist frequency.
        See also
        --------
        scipy.firwin
        """
        cutoff = np.atleast_1d(cutoff) / float(nyq)

        # Check for invalid input.
        if cutoff.ndim > 1:
            raise ValueError("The cutoff argument must be at most "
                             "one-dimensional.")
        if cutoff.size == 0:
            raise ValueError("At least one cutoff frequency must be given.")
        if cutoff.min() <= 0 or cutoff.max() >= 1:
            raise ValueError(
                "Invalid cutoff frequency {0}: frequencies must be "
                "greater than 0 and less than nyq.".format(cutoff))
        if np.any(np.diff(cutoff) <= 0):
            raise ValueError("Invalid cutoff frequencies: the frequencies "
                             "must be strictly increasing.")

        pass_nyquist = bool(cutoff.size & 1) ^ pass_zero
        if pass_nyquist and numtaps % 2 == 0:
            raise ValueError(
                "A filter with an even number of coefficients must "
                "have zero response at the Nyquist rate.")

        # Insert 0 and/or 1 at the ends of cutoff so that the length of cutoff
        # is even, and each pair in cutoff corresponds to passband.
        cutoff = np.hstack(([0.0] * pass_zero, cutoff, [1.0] * pass_nyquist))

        # `bands` is a 2D array; each row gives the left and right edges of
        # a passband.
        bands = cutoff.reshape(-1, 2)

        # Build up the coefficients.
        alpha = 0.5 * (numtaps - 1)
        m = np.arange(0, numtaps) - alpha
        h = 0
        for left, right in bands:
            h += right * sinc(right * m)
            h -= left * sinc(left * m)

        if type(window) == str:
            # Get and apply the window function.
            from scipy.signal.signaltools import get_window
            win = get_window(window, numtaps, fftbins=False)
        elif type(window) == np.ndarray:
            win = window
        else:
            logger.error(
                "The 'window' was neither a string nor a numpy array, it could not be evaluated."
            )
            return None
        # apply the window function.
        h *= win

        # Now handle scaling if desired.
        if scale:
            # Get the first passband.
            left, right = bands[0]
            if left == 0:
                scale_frequency = 0.0
            elif right == 1:
                scale_frequency = 1.0
            else:
                scale_frequency = 0.5 * (left + right)
            c = np.cos(np.pi * m * scale_frequency)
            s = np.sum(h * c)
            h /= s

        return h

    def _firwin_ord(self, F, W, A, alg):
        #http://www.mikroe.com/chapters/view/72/chapter-2-fir-filters/
        delta_f = abs(F[1] - F[0]) * 2  # referred to f_Ny
        delta_A = np.sqrt(A[0] * A[1])
        if self.fir_window_name == 'kaiser':
            N, beta = sig.kaiserord(20 * np.log10(np.abs(fb.fil[0]['A_SB'])),
                                    delta_f)
            self.ledWinPar1.setText(str(beta))
            fb.fil[0]['wdg_fil'][1] = beta
            self._update_UI()
        else:
            N = remezord(F, W, A, fs=1, alg=alg)[0]

        return N

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.N = self._firwin_ord([self.F_PB, self.F_SB], [1, 0],
                                  [self.A_PB, self.A_SB],
                                  alg=self.alg)
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        fil_dict['F_C'] = (self.F_SB + self.F_PB
                           ) / 2  # use average of calculated F_PB and F_SB
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        window=self.fir_window,
                        nyq=0.5))

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        window=self.fir_window,
                        nyq=0.5))

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        N = self._firwin_ord([self.F_SB, self.F_PB], [0, 1],
                             [self.A_SB, self.A_PB],
                             alg=self.alg)
        self.N = round_odd(N)  # enforce odd order
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        fil_dict['F_C'] = (self.F_SB + self.F_PB
                           ) / 2  # use average of calculated F_PB and F_SB
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        window=self.fir_window,
                        pass_zero=False,
                        nyq=0.5))

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        self.N = round_odd(self.N)  # enforce odd order
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        window=self.fir_window,
                        pass_zero=False,
                        nyq=0.5))

    # For BP and BS, F_PB and F_SB have two elements each
    def BPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.N = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2],
                          [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2],
                          fs=1,
                          alg=self.alg)[0]
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)

        fil_dict['F_C'] = (self.F_SB + self.F_PB
                           ) / 2  # use average of calculated F_PB and F_SB
        fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2
                            ) / 2  # use average of calculated F_PB and F_SB
        self._save(
            fil_dict,
            self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']],
                        window=self.fir_window,
                        pass_zero=False,
                        nyq=0.5))

    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        self._save(
            fil_dict,
            self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']],
                        window=self.fir_window,
                        pass_zero=False,
                        nyq=0.5))

    def BSmin(self, fil_dict):
        self._get_params(fil_dict)
        N = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2], [1, 0, 1],
                     [self.A_PB, self.A_SB, self.A_PB2],
                     fs=1,
                     alg=self.alg)[0]
        self.N = round_odd(N)  # enforce odd order
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        fil_dict['F_C'] = (self.F_SB + self.F_PB
                           ) / 2  # use average of calculated F_PB and F_SB
        fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2
                            ) / 2  # use average of calculated F_PB and F_SB
        self._save(
            fil_dict,
            self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']],
                        window=self.fir_window,
                        pass_zero=True,
                        nyq=0.5))

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        self.N = round_odd(self.N)  # enforce odd order
        if not self._test_N():
            return -1
        self.fir_window = calc_window_function(self.win_dict,
                                               self.fir_window_name,
                                               N=self.N,
                                               sym=True)
        self._save(
            fil_dict,
            self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']],
                        window=self.fir_window,
                        pass_zero=True,
                        nyq=0.5))

    #------------------------------------------------------------------------------
    def show_fft_win(self):
        """
        Pop-up FFT window
        """
        if self.but_fft_win.isChecked():
            qstyle_widget(self.but_fft_win, "changed")
        else:
            qstyle_widget(self.but_fft_win, "normal")

        if self.fft_window is None:  # no handle to the window? Create a new instance
            if self.but_fft_win.isChecked():
                # important: Handle to window must be class attribute
                # pass the name of the dictionary where parameters are stored and
                # whether a symmetric window or one that can be continued periodically
                # will be constructed
                self.fft_window = Plot_FFT_win(self,
                                               win_dict=self.win_dict,
                                               sym=True,
                                               title="pyFDA FIR Window Viewer")
                self.sig_tx.connect(self.fft_window.sig_rx)
                self.fft_window.sig_tx.connect(self.close_fft_win)
                self.fft_window.show(
                )  # modeless i.e. non-blocking popup window
        else:
            if not self.but_fft_win.isChecked():
                if self.fft_window is None:
                    logger.warning("FFT window is already closed!")
                else:
                    self.fft_window.close()

    def close_fft_win(self):
        self.fft_window = None
        self.but_fft_win.setChecked(False)
        qstyle_widget(self.but_fft_win, "normal")
Exemplo n.º 8
0
class Plot_Hf(QWidget):
    """
    Widget for plotting \|H(f)\|, frequency specs and the phase
    """
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self, parent):
        super(Plot_Hf, self).__init__(parent)
        self.needs_calc = True  # flag whether plot needs to be updated
        self.needs_draw = True  # flag whether plot needs to be redrawn
        self.tool_tip = "Magnitude and phase frequency response"
        self.tab_label = "|H(f)|"

        self.log_bottom = -80
        self.lin_neg_bottom = -10

        self._construct_ui()

#------------------------------------------------------------------------------

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("SIG_RX - needs_calc = {0}, vis = {1}\n{2}"\
                     .format(self.needs_calc, self.isVisible(), pprint_log(dict_sig)))

        if self.isVisible():
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\
                    or 'home' in dict_sig or self.needs_calc:
                self.draw()
                self.needs_calc = False
                self.needs_draw = False
            if 'view_changed' in dict_sig or self.needs_draw:
                self.update_view()
                self.needs_draw = False
        else:
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig:
                self.needs_calc = True
            if 'view_changed' in dict_sig:
                self.needs_draw = True

    def _construct_ui(self):
        """
        Define and construct the subwidgets
        """
        modes = ['| H |', 're{H}', 'im{H}']
        self.cmbShowH = QComboBox(self)
        self.cmbShowH.addItems(modes)
        self.cmbShowH.setObjectName("cmbUnitsH")
        self.cmbShowH.setToolTip(
            "Show magnitude, real / imag. part of H or H \n"
            "without linear phase (acausal system).")
        self.cmbShowH.setCurrentIndex(0)

        self.lblIn = QLabel("in", self)

        units = ['dB', 'V', 'W', 'Auto']
        self.cmbUnitsA = QComboBox(self)
        self.cmbUnitsA.addItems(units)
        self.cmbUnitsA.setObjectName("cmbUnitsA")
        self.cmbUnitsA.setToolTip(
            "<span>Set unit for y-axis:\n"
            "dB is attenuation (positive values), V and W are gain (less than 1).</span>"
        )
        self.cmbUnitsA.setCurrentIndex(0)

        self.lbl_log_bottom = QLabel("Bottom", self)
        self.led_log_bottom = QLineEdit(self)
        self.led_log_bottom.setText(str(self.log_bottom))
        self.led_log_bottom.setToolTip(
            "<span>Minimum display value for dB. scale.</span>")
        self.lbl_log_unit = QLabel("dB", self)

        self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkZerophase = QCheckBox("Zero phase", self)
        self.chkZerophase.setToolTip(
            "<span>Remove linear phase calculated from filter order.\n"
            "Attention: This makes no sense for a non-linear phase system!</span>"
        )

        self.lblInset = QLabel("Inset", self)
        self.cmbInset = QComboBox(self)
        self.cmbInset.addItems(['off', 'edit', 'fixed'])
        self.cmbInset.setObjectName("cmbInset")
        self.cmbInset.setToolTip("Display/edit second inset plot")
        self.cmbInset.setCurrentIndex(0)
        self.inset_idx = 0  # store previous index for comparison

        self.chkSpecs = QCheckBox("Specs", self)
        self.chkSpecs.setChecked(False)
        self.chkSpecs.setToolTip("Display filter specs as hatched regions")

        self.chkPhase = QCheckBox("Phase", self)
        self.chkPhase.setToolTip("Overlay phase")
        self.chkPhase.setChecked(False)

        self.chkAlign = QCheckBox("Align", self)
        self.chkAlign.setToolTip(
            "<span>Try to align grids for magnitude and phase "
            "(doesn't work in all cases).</span>")
        self.chkAlign.setChecked(True)
        self.chkAlign.setVisible(self.chkPhase.isChecked())

        #----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        #----------------------------------------------------------------------
        layHControls = QHBoxLayout()
        layHControls.addStretch(10)
        layHControls.addWidget(self.cmbShowH)
        layHControls.addWidget(self.lblIn)
        layHControls.addWidget(self.cmbUnitsA)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lbl_log_bottom)
        layHControls.addWidget(self.led_log_bottom)
        layHControls.addWidget(self.lbl_log_unit)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chkZerophase)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lblInset)
        layHControls.addWidget(self.cmbInset)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chkSpecs)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chkPhase)
        layHControls.addWidget(self.chkAlign)
        layHControls.addStretch(10)

        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layHControls)

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.mplwidget.mplToolbar.a_he.setEnabled(True)
        self.mplwidget.mplToolbar.a_he.info = "manual/plot_hf.html"
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self.draw()  # calculate and draw |H(f)|

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.cmbUnitsA.currentIndexChanged.connect(self.draw)
        self.led_log_bottom.editingFinished.connect(self.update_view)
        self.cmbShowH.currentIndexChanged.connect(self.draw)

        self.chkZerophase.clicked.connect(self.draw)
        self.cmbInset.currentIndexChanged.connect(self.draw_inset)

        self.chkSpecs.clicked.connect(self.draw)
        self.chkPhase.clicked.connect(self.draw)
        self.chkAlign.clicked.connect(self.draw)

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)

#------------------------------------------------------------------------------

    def init_axes(self):
        """
        Initialize and clear the axes (this is run only once)
        """
        if len(self.mplwidget.fig.get_axes()) == 0:  # empty figure, no axes
            self.ax = self.mplwidget.fig.subplots()
        self.ax.xaxis.tick_bottom()  # remove axis ticks on top
        self.ax.yaxis.tick_left()  # remove axis ticks right

#------------------------------------------------------------------------------

    def align_y_axes(self, ax1, ax2):
        """ Sets tick marks of twinx axes to line up with total number of
            ax1 tick marks
            """

        ax1_ylims = ax1.get_ybound()
        # collect only visible ticks
        ax1_yticks = [
            t for t in ax1.get_yticks()
            if t >= ax1_ylims[0] and t <= ax1_ylims[1]
        ]
        ax1_nticks = len(ax1_yticks)
        ax1_ydelta_lim = ax1_ylims[1] - ax1_ylims[0]  # span of limits
        ax1_ydelta_vis = ax1_yticks[-1] - ax1_yticks[
            0]  # delta of max. and min tick
        ax1_yoffset = ax1_yticks[0] - ax1_ylims[
            0]  # offset between lower limit and first tick

        # calculate scale of Delta Limits / Delta Ticks
        ax1_scale = ax1_ydelta_lim / ax1_ydelta_vis

        ax2_ylims = ax2.get_ybound()
        ax2_yticks = ax2.get_yticks()
        ax2_nticks = len(ax2_yticks)
        #ax2_ydelta_lim = ax2_ylims[1] - ax2_ylims[0]
        ax2_ydelta_vis = ax2_yticks[-1] - ax2_yticks[0]
        ax2_ydelta_lim = ax2_ydelta_vis * ax1_scale
        ax2_scale = ax2_ydelta_lim / ax2_ydelta_vis
        # calculate new offset between lower limit and first tick
        ax2_yoffset = ax1_yoffset * ax2_ydelta_lim / ax1_ydelta_lim
        logger.warning("ax2: delta_vis: {0}, scale: {1}, offset: {2}".format(
            ax2_ydelta_vis, ax2_scale, ax2_yoffset))
        logger.warning("Ticks: {0} # {1}".format(ax1_nticks, ax2_nticks))

        ax2.set_yticks(
            np.linspace(ax2_yticks[0], (ax2_yticks[1] - ax2_yticks[0]),
                        ax1_nticks))
        logger.warning("ax2[0]={0} | ax2[1]={1} ax2[-1]={2}".format(
            ax2_yticks[0], ax2_yticks[1], ax2_yticks[-1]))
        ax2_lim0 = ax2_yticks[0] - ax2_yoffset
        ax2.set_ybound(ax2_lim0, ax2_lim0 + ax2_ydelta_lim)

# =============================================================================
#             # https://stackoverflow.com/questions/26752464/how-do-i-align-gridlines-for-two-y-axis-scales-using-matplotlib
#             # works, but both axes have ugly numbers
#             nticks = 11
#             ax.yaxis.set_major_locator(ticker.LinearLocator(nticks))
#             self.ax_p.yaxis.set_major_locator(ticker.LinearLocator(nticks))
# # =============================================================================
# =============================================================================
#             # https://stackoverflow.com/questions/45037386/trouble-aligning-ticks-for-matplotlib-twinx-axes
#             # works, but second axis has ugly numbering
#             l_H = ax.get_ylim()
#             l_p = self.ax_p.get_ylim()
#             f = lambda x : l_p[0]+(x-l_H[0])/(l_H[1]-l_H[0])*(l_p[1]-l_p[0])
#             ticks = f(ax.get_yticks())
#             self.ax_p.yaxis.set_major_locator(ticker.FixedLocator(ticks))
#
# =============================================================================

# http://stackoverflow.com/questions/28692608/align-grid-lines-on-two-plots
# http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both
# http://stackoverflow.com/questions/20243683/matplotlib-align-twinx-tick-marks
# manual setting:
#self.ax_p.set_yticks( np.linspace(self.ax_p.get_ylim()[0],self.ax_p.get_ylim()[1],nbins) )
#ax1.set_yticks(np.linspace(ax1.get_ybound()[0], ax1.get_ybound()[1], 5))
#ax2.set_yticks(np.linspace(ax2.get_ybound()[0], ax2.get_ybound()[1], 5))
#http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both

# use helper functions from matplotlib.ticker:
#   MaxNLocator: set no more than nbins + 1 ticks
#self.ax_p.yaxis.set_major_locator( matplotlib.ticker.MaxNLocator(nbins = nbins) )
# further options: integer = False,
#                   prune = [‘lower’ | ‘upper’ | ‘both’ | None] Remove edge ticks
#   AutoLocator:
#self.ax_p.yaxis.set_major_locator( matplotlib.ticker.AutoLocator() )
#   LinearLocator:
#self.ax_p.yaxis.set_major_locator( matplotlib.ticker.LinearLocator(numticks = nbins -1 ) )

#            self.ax_p.locator_params(axis = 'y', nbins = nbins)
#
#            self.ax_p.set_yticks(np.linspace(self.ax_p.get_ybound()[0],
#                                             self.ax_p.get_ybound()[1],
#                                             len(self.ax.get_yticks())-1))

#N = source_ax.xaxis.get_major_ticks()
#target_ax.xaxis.set_major_locator(LinearLocator(N))

#------------------------------------------------------------------------------

    def plot_spec_limits(self, ax):
        """
        Plot the specifications limits (F_SB, A_SB, ...) as hatched areas with borders.
        """
        hatch = params['mpl_hatch']
        hatch_borders = params['mpl_hatch_border']

        def dB(lin):
            return 20 * np.log10(lin)

        def _plot_specs():
            # upper limits:
            ax.plot(F_lim_upl, A_lim_upl, F_lim_upc, A_lim_upc, F_lim_upr,
                    A_lim_upr, **hatch_borders)
            if A_lim_upl:
                ax.fill_between(F_lim_upl, max(A_lim_upl), A_lim_upl, **hatch)
            if A_lim_upc:
                ax.fill_between(F_lim_upc, max(A_lim_upc), A_lim_upc, **hatch)
            if A_lim_upr:
                ax.fill_between(F_lim_upr, max(A_lim_upr), A_lim_upr, **hatch)
            # lower limits:
            ax.plot(F_lim_lol, A_lim_lol, F_lim_loc, A_lim_loc, F_lim_lor,
                    A_lim_lor, **hatch_borders)
            if A_lim_lol:
                ax.fill_between(F_lim_lol, min(A_lim_lol), A_lim_lol, **hatch)
            if A_lim_loc:
                ax.fill_between(F_lim_loc, min(A_lim_loc), A_lim_loc, **hatch)
            if A_lim_lor:
                ax.fill_between(F_lim_lor, min(A_lim_lor), A_lim_lor, **hatch)

        if self.unitA == 'V':
            exp = 1.
        elif self.unitA == 'W':
            exp = 2.

        if self.unitA == 'dB':
            if fb.fil[0]['ft'] == "FIR":
                A_PB_max = dB(1 + self.A_PB)
                A_PB2_max = dB(1 + self.A_PB2)
            else:  # IIR dB
                A_PB_max = A_PB2_max = 0

            A_PB_min = dB(1 - self.A_PB)
            A_PB2_min = dB(1 - self.A_PB2)
            A_PB_minx = min(A_PB_min, A_PB2_min) - 5
            A_PB_maxx = max(A_PB_max, A_PB2_max) + 5

            A_SB = dB(self.A_SB)
            A_SB2 = dB(self.A_SB2)
            A_SB_maxx = max(A_SB, A_SB2) + 10
        else:  # 'V' or 'W'
            if fb.fil[0]['ft'] == "FIR":
                A_PB_max = (1 + self.A_PB)**exp
                A_PB2_max = (1 + self.A_PB2)**exp
            else:  # IIR lin
                A_PB_max = A_PB2_max = 1

            A_PB_min = (1 - self.A_PB)**exp
            A_PB2_min = (1 - self.A_PB2)**exp
            A_PB_minx = min(A_PB_min, A_PB2_min) / 1.05
            A_PB_maxx = max(A_PB_max, A_PB2_max) * 1.05

            A_SB = self.A_SB**exp
            A_SB2 = self.A_SB2**exp
            A_SB_maxx = A_PB_min / 10.

        F_max = self.f_max / 2
        F_PB = self.F_PB
        F_SB = fb.fil[0]['F_SB'] * self.f_max
        F_SB2 = fb.fil[0]['F_SB2'] * self.f_max
        F_PB2 = fb.fil[0]['F_PB2'] * self.f_max

        F_lim_upl = F_lim_lol = []  # left side limits, lower and upper
        A_lim_upl = A_lim_lol = []

        F_lim_upc = F_lim_loc = []  # center limits, lower and upper
        A_lim_upc = A_lim_loc = []

        F_lim_upr = F_lim_lor = []  # right side limits, lower and upper
        A_lim_upr = A_lim_lor = []

        if fb.fil[0]['rt'] == 'LP':
            F_lim_upl = [0, F_PB, F_PB]
            A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx]
            F_lim_lol = F_lim_upl
            A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx]

            F_lim_upr = [F_SB, F_SB, F_max]
            A_lim_upr = [A_SB_maxx, A_SB, A_SB]

        if fb.fil[0]['rt'] == 'HP':
            F_lim_upl = [0, F_SB, F_SB]
            A_lim_upl = [A_SB, A_SB, A_SB_maxx]

            F_lim_upr = [F_PB, F_PB, F_max]
            A_lim_upr = [A_PB_maxx, A_PB_max, A_PB_max]
            F_lim_lor = F_lim_upr
            A_lim_lor = [A_PB_minx, A_PB_min, A_PB_min]

        if fb.fil[0]['rt'] == 'BS':
            F_lim_upl = [0, F_PB, F_PB]
            A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx]
            F_lim_lol = F_lim_upl
            A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx]

            F_lim_upc = [F_SB, F_SB, F_SB2, F_SB2]
            A_lim_upc = [A_SB_maxx, A_SB, A_SB, A_SB_maxx]

            F_lim_upr = [F_PB2, F_PB2, F_max]
            A_lim_upr = [A_PB_maxx, A_PB2_max, A_PB2_max]
            F_lim_lor = F_lim_upr
            A_lim_lor = [A_PB_minx, A_PB2_min, A_PB2_min]

        if fb.fil[0]['rt'] == 'BP':
            F_lim_upl = [0, F_SB, F_SB]
            A_lim_upl = [A_SB, A_SB, A_SB_maxx]

            F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2]
            A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx]
            F_lim_loc = F_lim_upc
            A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx]

            F_lim_upr = [F_SB2, F_SB2, F_max]
            A_lim_upr = [A_SB_maxx, A_SB2, A_SB2]

        if fb.fil[0]['rt'] == 'HIL':
            F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2]
            A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx]

            F_lim_loc = F_lim_upc
            A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx]

        F_lim_upr = np.array(F_lim_upr)
        F_lim_lor = np.array(F_lim_lor)
        F_lim_upl = np.array(F_lim_upl)
        F_lim_lol = np.array(F_lim_lol)
        F_lim_upc = np.array(F_lim_upc)
        F_lim_loc = np.array(F_lim_loc)

        _plot_specs()  # plot specs in the range 0 ... f_S/2

        if fb.fil[0]['freqSpecsRangeType'] != 'half':
            # add plot limits for other half of the spectrum
            if fb.fil[0][
                    'freqSpecsRangeType'] == 'sym':  # frequency axis +/- f_S/2
                F_lim_upl = -F_lim_upl
                F_lim_lol = -F_lim_lol
                F_lim_upc = -F_lim_upc
                F_lim_loc = -F_lim_loc
                F_lim_upr = -F_lim_upr
                F_lim_lor = -F_lim_lor
            else:  # -> 'whole'
                F_lim_upl = self.f_max - F_lim_upl
                F_lim_lol = self.f_max - F_lim_lol
                F_lim_upc = self.f_max - F_lim_upc
                F_lim_loc = self.f_max - F_lim_loc
                F_lim_upr = self.f_max - F_lim_upr
                F_lim_lor = self.f_max - F_lim_lor

            _plot_specs()

#------------------------------------------------------------------------------

    def draw_inset(self):
        """
        Construct / destruct second axes for an inset second plot
        """
        # TODO:  try   ax1 = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
        # TODO: choose size & position of inset, maybe dependent on filter type
        #        or specs (i.e. where is passband etc.)

        # DEBUG
        #            print(self.cmbInset.currentIndex(), self.mplwidget.fig.axes) # list of axes in Figure
        #            for ax in self.mplwidget.fig.axes:
        #                print(ax)
        #                print("cmbInset, inset_idx:",self.cmbInset.currentIndex(), self.inset_idx)

        if self.cmbInset.currentIndex() > 0:
            if self.inset_idx == 0:
                # Inset was turned off before, create a new one
                #  Add an axes at position rect [left, bottom, width, height]:
                self.ax_i = self.mplwidget.fig.add_axes([0.65, 0.61, .3, .3])
                self.ax_i.clear()  # clear old plot and specs

                # draw an opaque background with the extent of the inset plot:
                #                self.ax_i.patch.set_facecolor('green') # without label area
                #                self.mplwidget.fig.patch.set_facecolor('green') # whole figure
                extent = self.mplwidget.get_full_extent(self.ax_i, pad=0.0)
                # Transform this back to figure coordinates - otherwise, it
                #  won't behave correctly when the size of the plot is changed:
                extent = extent.transformed(
                    self.mplwidget.fig.transFigure.inverted())
                rect = Rectangle((extent.xmin, extent.ymin),
                                 extent.width,
                                 extent.height,
                                 facecolor=rcParams['figure.facecolor'],
                                 edgecolor='none',
                                 transform=self.mplwidget.fig.transFigure,
                                 zorder=-1)
                self.ax_i.patches.append(rect)

                self.ax_i.set_xlim(fb.fil[0]['freqSpecsRange'])
                self.ax_i.plot(self.F, self.H_plt)

            if self.cmbInset.currentIndex() == 1:  # edit / navigate inset
                self.ax_i.set_navigate(True)
                self.ax.set_navigate(False)
                if self.chkSpecs.isChecked():
                    self.plot_spec_limits(self.ax_i)
            else:  # edit / navigate main plot
                self.ax_i.set_navigate(False)
                self.ax.set_navigate(True)
        else:  # inset has been turned off, delete it
            self.ax.set_navigate(True)
            try:
                #remove ax_i from the figure
                self.mplwidget.fig.delaxes(self.ax_i)
            except AttributeError:
                pass

        self.inset_idx = self.cmbInset.currentIndex()  # update index
        self.draw()

#------------------------------------------------------------------------------

    def draw_phase(self, ax):
        """
        Draw phase on second y-axis in the axes system passed as the argument
        """
        if hasattr(self, 'ax_p'):
            self.mplwidget.fig.delaxes(self.ax_p)
            del self.ax_p
        # try:
        #     self.mplwidget.fig.delaxes(self.ax_p)
        # except (KeyError, AttributeError):
        #     pass

        if self.chkPhase.isChecked():
            self.ax_p = ax.twinx(
            )  # second axes system with same x-axis for phase
            self.ax_p.is_twin = True  # mark this as 'twin' to suppress second grid in mpl_widget
            #
            phi_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$'
            if fb.fil[0]['plt_phiUnit'] == 'rad':
                phi_str += ' in rad ' + r'$\rightarrow $'
                scale = 1.
            elif fb.fil[0]['plt_phiUnit'] == 'rad/pi':
                phi_str += ' in rad' + r'$ / \pi \;\rightarrow $'
                scale = 1. / np.pi
            else:
                phi_str += ' in deg ' + r'$\rightarrow $'
                scale = 180. / np.pi

            # replace nan and inf by finite values, otherwise np.unwrap yields
            # an array full of nans
            phi = np.angle(np.nan_to_num(self.H_c))
            #-----------------------------------------------------------
            self.ax_p.plot(self.F,
                           np.unwrap(phi) * scale,
                           'g-.',
                           label="Phase")
            #-----------------------------------------------------------
            self.ax_p.set_ylabel(phi_str)

#------------------------------------------------------------------------------

    def calc_hf(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """

        # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], True)

#------------------------------------------------------------------------------

    def draw(self):
        """
        Re-calculate \|H(f)\| and draw the figure
        """
        self.chkAlign.setVisible(self.chkPhase.isChecked())
        self.calc_hf()
        self.update_view()

#------------------------------------------------------------------------------

    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        # suppress "divide by zero in log10" warnings
        old_settings_seterr = np.seterr()
        np.seterr(divide='ignore')

        # Get corners for spec display from the parameters of the target specs subwidget
        try:
            param_list = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']]\
                                    [fb.fil[0]['fc']][fb.fil[0]['fo']]['tspecs'][1]['amp']
        except KeyError:
            param_list = []

        SB = [l for l in param_list if 'A_SB' in l]
        PB = [l for l in param_list if 'A_PB' in l]

        if SB:
            A_min = min([fb.fil[0][l] for l in SB])
        else:
            A_min = 5e-4

        if PB:
            A_max = max([fb.fil[0][l] for l in PB])
        else:
            A_max = 1

        if np.all(self.W) is None:  # H(f) has not been calculated yet
            self.calc_hf()

        if self.cmbUnitsA.currentText() == 'Auto':
            self.unitA = fb.fil[0]['amp_specs_unit']
        else:
            self.unitA = self.cmbUnitsA.currentText()

        # only display log bottom widget for unit dB
        self.lbl_log_bottom.setVisible(self.unitA == 'dB')
        self.led_log_bottom.setVisible(self.unitA == 'dB')
        self.lbl_log_unit.setVisible(self.unitA == 'dB')

        # Linphase settings only makes sense for amplitude plot and
        # for plottin real/imag. part of H, not its magnitude
        self.chkZerophase.setCheckable(self.unitA == 'V')
        self.chkZerophase.setEnabled(self.unitA == 'V')

        self.specs = self.chkSpecs.isChecked()

        self.f_max = fb.fil[0]['f_max']

        self.F_PB = fb.fil[0]['F_PB'] * self.f_max
        self.f_maxB = fb.fil[0]['F_SB'] * self.f_max

        self.A_PB = fb.fil[0]['A_PB']
        self.A_PB2 = fb.fil[0]['A_PB2']
        self.A_SB = fb.fil[0]['A_SB']
        self.A_SB2 = fb.fil[0]['A_SB2']

        f_lim = fb.fil[0]['freqSpecsRange']

        #========= select frequency range to be displayed =====================
        #=== shift, scale and select: W -> F, H_cplx -> H_c
        self.F = self.W / (2 * np.pi) * self.f_max

        if fb.fil[0]['freqSpecsRangeType'] == 'sym':
            # shift H and F by f_S/2
            self.H_c = np.fft.fftshift(self.H_cmplx)
            self.F -= self.f_max / 2.
        elif fb.fil[0]['freqSpecsRangeType'] == 'half':
            # only use the first half of H and F
            self.H_c = self.H_cmplx[0:params['N_FFT'] // 2]
            self.F = self.F[0:params['N_FFT'] // 2]
        else:  # fb.fil[0]['freqSpecsRangeType'] == 'whole'
            # use H and F as calculated
            self.H_c = self.H_cmplx

        # now calculate mag / real / imaginary part of H_c:
        if self.chkZerophase.isChecked():  # remove the linear phase
            self.H_c = self.H_c * np.exp(
                1j * self.W[0:len(self.F)] * fb.fil[0]["N"] / 2.)

        if self.cmbShowH.currentIndex() == 0:  # show magnitude of H
            H = abs(self.H_c)
            H_str = r'$|H(\mathrm{e}^{\mathrm{j} \Omega})|$'
        elif self.cmbShowH.currentIndex() == 1:  # show real part of H
            H = self.H_c.real
            H_str = r'$\Re \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$'
        else:  # show imag. part of H
            H = self.H_c.imag
            H_str = r'$\Im \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$'

        #================ Main Plotting Routine =========================
        #===  clear the axes and (re)draw the plot (if selectable)
        if self.ax.get_navigate():

            if self.unitA == 'dB':
                self.log_bottom = safe_eval(self.led_log_bottom.text(),
                                            self.log_bottom,
                                            return_type='float',
                                            sign='neg')
                self.led_log_bottom.setText(str(self.log_bottom))

                self.H_plt = np.maximum(20 * np.log10(abs(H)), self.log_bottom)
                A_lim = [self.log_bottom, 2]
                H_str += ' in dB ' + r'$\rightarrow$'
            elif self.unitA == 'V':  #  'lin'
                self.H_plt = H
                if self.cmbShowH.currentIndex(
                ) != 0:  # H can be less than zero
                    A_min = max(self.lin_neg_bottom,
                                np.nanmin(self.H_plt[np.isfinite(self.H_plt)]))
                else:
                    A_min = 0
                A_lim = [A_min, (1.05 + A_max)]
                H_str += ' in V ' + r'$\rightarrow $'
                self.ax.axhline(linewidth=1, color='k')  # horizontal line at 0
            else:  # unit is W
                A_lim = [0, (1.03 + A_max)**2.]
                self.H_plt = H * H.conj()
                H_str += ' in W ' + r'$\rightarrow $'

            #logger.debug("lim: {0}, min: {1}, max: {2} - {3}".format(A_lim, A_min, A_max, self.H_plt[0]))

            #-----------------------------------------------------------
            self.ax.clear()
            self.ax.plot(self.F, self.H_plt, label='H(f)')
            # TODO: self.draw_inset() # this gives an infinite recursion
            self.draw_phase(self.ax)
            #-----------------------------------------------------------

            #============= Set Limits and draw specs =========================
            if self.chkSpecs.isChecked():
                self.plot_spec_limits(self.ax)

            #     self.ax_bounds = [self.ax.get_ybound()[0], self.ax.get_ybound()[1]]#, self.ax.get]
            self.ax.set_xlim(f_lim)
            self.ax.set_ylim(A_lim)
            # logger.warning("set limits")

            self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
            self.ax.set_ylabel(H_str)
            if self.chkPhase.isChecked():
                self.ax.set_title(r'Magnitude and Phase Frequency Response')
            else:
                self.ax.set_title(r'Magnitude Frequency Response')
            self.ax.xaxis.set_minor_locator(
                AutoMinorLocator())  # enable minor ticks
            self.ax.yaxis.set_minor_locator(
                AutoMinorLocator())  # enable minor ticks

            np.seterr(**old_settings_seterr)

        self.redraw()

#------------------------------------------------------------------------------

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        if hasattr(self, 'ax_p') and self.chkAlign.isChecked():
            # Align gridlines between H(f) and phi nicely
            self.align_y_axes(self.ax, self.ax_p)
        self.mplwidget.redraw()
Exemplo n.º 9
0
class AllpPZ(QWidget):

    FRMT = 'zpk'  # output format of delay filter widget

    info = """
**Allpass widget**

allows entering the two **poles** :math:`p`. **zeros** are calculated from the 
reciprocal values of the poles. There is no minimum algorithm, only the two 
poles can be entered manually.

    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.p = [0.5, 0.5j]

        self.ft = 'IIR'

        # the following defines which subwidgets are "a"ctive, "i"nvisible or "d"eactivated
        self.rt_dicts = ('com', )
        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter poles  <b><i>p</i></b> for allpass function,"
                     "zeros will be calculated.</span>")
                },
            },
            'AP': {
                'man': {}
            }
        }

        self.info_doc = []

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in 
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        self.lbl_pole1 = QLabel("Pole 1", self)
        self.lbl_pole1.setObjectName('wdg_lbl_pole1')
        self.led_pole1 = QLineEdit(self)
        self.led_pole1.setText(str(self.p[0]))
        self.led_pole1.setObjectName('wdg_led_pole1')
        self.led_pole1.setToolTip("Pole 1 for allpass filter")

        self.lbl_pole2 = QLabel("Pole 2", self)
        self.lbl_pole2.setObjectName('wdg_lbl_pole2')
        self.led_pole2 = QLineEdit(self)
        self.led_pole2.setText(str(self.p[1]))
        self.led_pole2.setObjectName('wdg_led_pole2')
        self.led_pole2.setToolTip("Pole 2 for allpass filter")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_pole1)
        self.layHWin.addWidget(self.led_pole1)
        self.layHWin.addWidget(self.lbl_pole2)
        self.layHWin.addWidget(self.led_pole2)
        self.layHWin.setContentsMargins(0, 0, 0, 0)
        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.led_pole1.editingFinished.connect(self._update_UI)
        self.led_pole2.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.p[0] = safe_eval(self.led_pole1.text(),
                              self.p[0],
                              return_type='cmplx')
        self.led_pole1.setText(str(self.p[0]))

        self.p[1] = safe_eval(self.led_pole2.text(),
                              self.p[1],
                              return_type='cmplx')
        self.led_pole2.setText(str(self.p[1]))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'poles': {
                'p1': self.p[0],
                'p2': self.p[1]
            }})

        # sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'pole_1_2'})

    def _load_dict(self):
        """
        Reload parameter(s) from filter dictionary (if they exist) and set 
        corresponding UI elements. _load_dict() is called upon initialization
        and when the filter is loaded from disk.
        """
        if 'wdg_fil' in fb.fil[0] and 'poles' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['poles']
            if 'p1' in wdg_fil_par:
                self.p1 = wdg_fil_par['p1']
                self.led_pole1.setText(str(self.p1))
            if 'p2' in wdg_fil_par:
                self.p2 = wdg_fil_par['p2']
                self.led_pole2.setText(str(self.p2))

    def _get_params(self, fil_dict):
        """
        Get parameters needed for filter design from the passed dictionary and 
        translate them to instance parameters, scaling / transforming them if needed.
        """
        #self.p1     = fil_dict['zpk'][1][0]  # get the first and second pole
        #self.p2     = fil_dict['zpk'][1][1]  # from central filter dect
        logger.info(fil_dict['zpk'])

    def _test_poles(self):
        """
        Warn the user if one of the poles is outside the unit circle
        """
        if abs(self.p[0]) >= 1 or abs(self.p[1]) >= 1:
            return qfilter_warning(self, self.p[0], "Delay")
        else:
            return True

    def _save(self, fil_dict, arg=None):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        if arg is None:
            logger.error("Passed empty filter dict")
        logger.info(arg)
        fil_save(fil_dict, arg, self.FRMT, __name__)

        fil_dict['N'] = len(self.p)

#------------------------------------------------------------------------------
# Filter design routines
#------------------------------------------------------------------------------
# The method name MUST be "FilterType"+"MinMan", e.g. LPmin or BPman

    def APman(self, fil_dict):
        """
        Calculate z =1/p* for a given set of poles p. If p=0, set z=0.
        The gain factor k is calculated from z and p at z = 1.
        """
        self._get_params(fil_dict)  # not needed here
        if not self._test_poles():
            return -1
        self.z = [0, 0]
        if self.p[0] != 0:
            self.z[0] = np.conj(1 / self.p[0])
        if type(self.p[0]) == complex:
            pass
        if self.p[1] != 0:
            self.z[1] = np.conj(1 / self.p[1])

        k = np.abs(
            np.polyval(np.poly(self.p), 1) / np.polyval(np.poly(self.z), 1))
        zpk_list = [self.z, self.p, k]

        self._save(fil_dict, zpk_list)
Exemplo n.º 10
0
class SelectFilter(QWidget):
    """
    Construct and read combo boxes for selecting the filter, consisting of the
    following hierarchy:

    1. Response Type rt (LP, HP, Hilbert, ...)
    2. Filter Type ft (IIR, FIR, CIC ...)
    3. Filter Class (Butterworth, ...)

    Every time a combo box is changed manually, the filter tree for the selected
    response resp. filter type is read and the combo box(es) further down in
    the hierarchy are populated according to the available combinations.

    sig_tx({'filt_changed'}) is emitted and propagated to input_filter_specs.py
    where it triggers the recreation of all subwidgets.
    """
    sig_tx = pyqtSignal(object)  # outgoing
    from pyfda.libs.pyfda_qt_lib import emit

    def __init__(self, parent=None):
        super(SelectFilter, self).__init__(parent)

        self.fc_last = ''  # previous filter class

        self._construct_UI()

        self._set_response_type()  # first time initialization

    def _construct_UI(self):
        """
        Construct UI with comboboxes for selecting filter:

        - cmbResponseType for selecting response type rt (LP, HP, ...)

        - cmbFilterType for selection of filter type (IIR, FIR, ...)

        - cmbFilterClass for selection of design design class (Chebyshev, ...)

        and populate them from the "filterTree" dict during the initial run.
        Later, calling _set_response_type() updates the three combo boxes.

        See filterbroker.py for structure and content of "filterTree" dict

        """
        # ----------------------------------------------------------------------
        # Combo boxes for filter selection
        # ----------------------------------------------------------------------
        self.cmbResponseType = QComboBox(self)
        self.cmbResponseType.setObjectName("comboResponseType")
        self.cmbResponseType.setToolTip("Select filter response type.")
        self.cmbFilterType = QComboBox(self)
        self.cmbFilterType.setObjectName("comboFilterType")
        self.cmbFilterType.setToolTip(
            "<span>Choose filter type, either recursive (Infinite Impulse Response) "
            "or transversal (Finite Impulse Response).</span>")
        self.cmbFilterClass = QComboBox(self)
        self.cmbFilterClass.setObjectName("comboFilterClass")
        self.cmbFilterClass.setToolTip("Select the filter design class.")

        # Adapt comboboxes size dynamically to largest element
        self.cmbResponseType.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbFilterType.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbFilterClass.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        # ----------------------------------------------------------------------
        # Populate combo box with initial settings from fb.fil_tree
        # ----------------------------------------------------------------------
        # Translate short response type ("LP") to displayed names ("Lowpass")
        # (correspondence is defined in pyfda_rc.py) and populate rt combo box
        #
        rt_list = sorted(list(fb.fil_tree.keys()))

        for rt in rt_list:
            try:
                self.cmbResponseType.addItem(rc.rt_names[rt], rt)
            except KeyError as e:
                logger.warning(
                    f"KeyError: {e} has no corresponding full name in rc.rt_names."
                )
        idx = self.cmbResponseType.findData('LP')  # find index for 'LP'

        if idx == -1:  # Key 'LP' does not exist, use first entry instead
            idx = 0

        self.cmbResponseType.setCurrentIndex(idx)  # set initial index
        rt = qget_cmb_box(self.cmbResponseType)

        for ft in fb.fil_tree[rt]:
            self.cmbFilterType.addItem(rc.ft_names[ft], ft)
        self.cmbFilterType.setCurrentIndex(0)  # set initial index
        ft = qget_cmb_box(self.cmbFilterType)

        for fc in fb.fil_tree[rt][ft]:
            self.cmbFilterClass.addItem(fb.filter_classes[fc]['name'], fc)
        self.cmbFilterClass.setCurrentIndex(0)  # set initial index

        # ----------------------------------------------------------------------
        # Layout for Filter Type Subwidgets
        # ----------------------------------------------------------------------

        layHFilWdg = QHBoxLayout()  # container for filter subwidgets
        layHFilWdg.addWidget(self.cmbResponseType)  # LP, HP, BP, etc.
        layHFilWdg.addStretch()
        layHFilWdg.addWidget(self.cmbFilterType)  # FIR, IIR
        layHFilWdg.addStretch()
        layHFilWdg.addWidget(self.cmbFilterClass)  # bessel, elliptic, etc.

        # ----------------------------------------------------------------------
        # Layout for dynamic filter subwidgets (empty frame)
        # ----------------------------------------------------------------------
        # see Summerfield p. 278
        self.layHDynWdg = QHBoxLayout()  # for additional dynamic subwidgets

        # ----------------------------------------------------------------------
        # Filter Order Subwidgets
        # ----------------------------------------------------------------------
        self.lblOrder = QLabel("<b>Order:</b>")
        self.chkMinOrder = QCheckBox("Minimum", self)
        self.chkMinOrder.setToolTip(
            "<span>Minimum filter order / # of taps is determined automatically.</span>"
        )
        self.lblOrderN = QLabel("<b><i>N =</i></b>")
        self.ledOrderN = QLineEdit(str(fb.fil[0]['N']), self)
        self.ledOrderN.setToolTip("Filter order (# of taps - 1).")

        # --------------------------------------------------
        #  Layout for filter order subwidgets
        # --------------------------------------------------
        layHOrdWdg = QHBoxLayout()
        layHOrdWdg.addWidget(self.lblOrder)
        layHOrdWdg.addWidget(self.chkMinOrder)
        layHOrdWdg.addStretch()
        layHOrdWdg.addWidget(self.lblOrderN)
        layHOrdWdg.addWidget(self.ledOrderN)

        # ----------------------------------------------------------------------
        # OVERALL LAYOUT (stack standard + dynamic subwidgets vertically)
        # ----------------------------------------------------------------------
        self.layVAllWdg = QVBoxLayout()
        self.layVAllWdg.addLayout(layHFilWdg)
        self.layVAllWdg.addLayout(self.layHDynWdg)
        self.layVAllWdg.addLayout(layHOrdWdg)

        # ==============================================================================
        frmMain = QFrame(self)
        frmMain.setLayout(self.layVAllWdg)

        layHMain = QHBoxLayout()
        layHMain.addWidget(frmMain)
        layHMain.setContentsMargins(*rc.params['wdg_margins'])

        self.setLayout(layHMain)

        # ==============================================================================

        # ------------------------------------------------------------
        # SIGNALS & SLOTS
        # ------------------------------------------------------------
        # Connect comboBoxes and setters, propgate change events hierarchically
        #  through all widget methods and emit 'filt_changed' in the end.
        self.cmbResponseType.currentIndexChanged.connect(
            lambda: self._set_response_type(enb_signal=True))  # 'LP'
        self.cmbFilterType.currentIndexChanged.connect(
            lambda: self._set_filter_type(enb_signal=True))  # 'IIR'
        self.cmbFilterClass.currentIndexChanged.connect(
            lambda: self._set_design_method(enb_signal=True))  # 'cheby1'
        self.chkMinOrder.clicked.connect(
            lambda: self._set_filter_order(enb_signal=True))  # Min. Order
        self.ledOrderN.editingFinished.connect(
            lambda: self._set_filter_order(enb_signal=True))  # Manual Order
        # ------------------------------------------------------------

# ------------------------------------------------------------------------------

    def load_dict(self):
        """
        Reload comboboxes from filter dictionary to update changed settings
        after loading a filter design from disk.
        `load_dict` uses the automatism of _set_response_type etc.
        of checking whether the previously selected filter design method is
        also available for the new combination.
        """
        # find index for response type:
        rt_idx = self.cmbResponseType.findData(fb.fil[0]['rt'])
        self.cmbResponseType.setCurrentIndex(rt_idx)
        self._set_response_type()

# ------------------------------------------------------------------------------

    def _set_response_type(self, enb_signal=False):
        """
        Triggered when cmbResponseType (LP, HP, ...) is changed:
        Copy selection to self.rt and fb.fil[0] and reconstruct filter type combo

        If previous filter type (FIR, IIR, ...) exists for new rt, set the
        filter type combo box to the old setting
        """
        # Read current setting of comboBox as string and store it in the filter dict
        fb.fil[0]['rt'] = self.rt = qget_cmb_box(self.cmbResponseType)

        # Get list of available filter types for new rt
        ft_list = list(
            fb.fil_tree[self.rt].keys())  # explicit list() needed for Py3
        # ---------------------------------------------------------------
        # Rebuild filter type combobox entries for new rt setting
        self.cmbFilterType.blockSignals(
            True)  # don't fire when changed programmatically
        self.cmbFilterType.clear()
        for ft in fb.fil_tree[self.rt]:
            self.cmbFilterType.addItem(rc.ft_names[ft], ft)

        # Is current filter type (e.g. IIR) in list for new rt?
        if fb.fil[0]['ft'] in ft_list:
            ft_idx = self.cmbFilterType.findText(fb.fil[0]['ft'])
            self.cmbFilterType.setCurrentIndex(
                ft_idx)  # yes, set same ft as before
        else:
            self.cmbFilterType.setCurrentIndex(0)  # no, set index 0

        self.cmbFilterType.blockSignals(False)
        # ---------------------------------------------------------------

        self._set_filter_type(enb_signal)

# ------------------------------------------------------------------------------

    def _set_filter_type(self, enb_signal=False):
        """"
        Triggered when cmbFilterType (IIR, FIR, ...) is changed:
        - read filter type ft and copy it to fb.fil[0]['ft'] and self.ft
        - (re)construct design method combo, adding
          displayed text (e.g. "Chebyshev 1") and hidden data (e.g. "cheby1")
        """
        # Read out current setting of comboBox and convert to string
        fb.fil[0]['ft'] = self.ft = qget_cmb_box(self.cmbFilterType)
        #
        logger.debug("InputFilter.set_filter_type triggered: {0}".format(
            self.ft))

        # ---------------------------------------------------------------
        # Get all available design methods for new ft from fil_tree and
        # - Collect them in fc_list
        # - Rebuild design method combobox entries for new ft setting:
        #    The combobox is populated with the "long name",
        #    the internal name is stored in comboBox.itemData
        self.cmbFilterClass.blockSignals(True)
        self.cmbFilterClass.clear()
        fc_list = []

        for fc in sorted(fb.fil_tree[self.rt][self.ft]):
            self.cmbFilterClass.addItem(fb.filter_classes[fc]['name'], fc)
            fc_list.append(fc)

        logger.debug("fc_list: {0}\n{1}".format(fc_list, fb.fil[0]['fc']))

        # Does new ft also provide the previous design method (e.g. ellip)?
        # Has filter been instantiated?
        if fb.fil[0]['fc'] in fc_list and ff.fil_inst:
            # yes, set same fc as before
            fc_idx = self.cmbFilterClass.findText(
                fb.filter_classes[fb.fil[0]['fc']]['name'])
            logger.debug("fc_idx : %s", fc_idx)
            self.cmbFilterClass.setCurrentIndex(fc_idx)
        else:
            self.cmbFilterClass.setCurrentIndex(0)  # no, set index 0

        self.cmbFilterClass.blockSignals(False)

        self._set_design_method(enb_signal)

# ------------------------------------------------------------------------------

    def _set_design_method(self, enb_signal=False):
        """
        Triggered when cmbFilterClass (cheby1, ...) is changed:
        - read design method fc and copy it to fb.fil[0]
        - create / update global filter instance fb.fil_inst of fc class
        - update dynamic widgets (if fc has changed and if there are any)
        - call load filter order
        """
        fb.fil[0]['fc'] = fc = qget_cmb_box(self.cmbFilterClass)

        if fc != self.fc_last:  # fc has changed:

            # when filter has been changed, try to destroy dynamic widgets of last fc:
            if self.fc_last:
                self._destruct_dyn_widgets()

            # ==================================================================
            """
            Create new instance of the selected filter class, accessible via
            its handle fb.fil_inst
            """
            err = ff.fil_factory.create_fil_inst(fc)
            logger.debug(f"InputFilter.set_design_method triggered: {fc}\n"
                         f"Returned error code {err}")
            # ==================================================================

            # Check whether new design method also provides the old filter order
            # method. If yes, don't change it, else set first available
            # filter order method
            if fb.fil[0]['fo'] not in fb.fil_tree[self.rt][self.ft][fc].keys():
                fb.fil[0].update({'fo': {}})
                # explicit list(dict.keys()) needed for Python 3
                fb.fil[0]['fo'] = list(
                    fb.fil_tree[self.rt][self.ft][fc].keys())[0]

# =============================================================================
#             logger.debug("selFilter = %s"
#                    "filterTree[fc] = %s"
#                    "filterTree[fc].keys() = %s"
#                   %(fb.fil[0], fb.fil_tree[self.rt][self.ft][fc],\
#                     fb.fil_tree[self.rt][self.ft][fc].keys()
#                     ))
#
# =============================================================================
# construct dyn. subwidgets if available
            if hasattr(ff.fil_inst, 'construct_UI'):
                self._construct_dyn_widgets()

            self.fc_last = fb.fil[0]['fc']

        self.load_filter_order(enb_signal)

# ------------------------------------------------------------------------------

    def load_filter_order(self, enb_signal=False):
        """
        Called by set_design_method or from InputSpecs (with enb_signal = False),
          load filter order setting from fb.fil[0] and update widgets

        """
        # collect dict_keys of available filter order [fo] methods for selected
        # design method [fc] from fil_tree (explicit list() needed for Python 3)
        fo_dict = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']][fb.fil[0]
                                                                ['fc']]
        fo_list = list(fo_dict.keys())

        # is currently selected fo setting available for (new) fc ?
        if fb.fil[0]['fo'] in fo_list:
            self.fo = fb.fil[0]['fo']  # keep current setting
        else:
            self.fo = fo_list[0]  # use first list entry from filterTree
            fb.fil[0]['fo'] = self.fo  # and update fo method

        # check whether fo widget is active, disabled or invisible
        if 'fo' in fo_dict[self.fo] and len(fo_dict[self.fo]['fo']) > 1:
            status = fo_dict[self.fo]['fo'][0]
        else:
            status = 'i'

        # Determine which subwidgets are __visible__
        self.chkMinOrder.setVisible('min' in fo_list)
        self.ledOrderN.setVisible(status in {'a', 'd'})
        self.lblOrderN.setVisible(status in {'a', 'd'})

        # Determine which subwidgets are __enabled__
        self.chkMinOrder.setChecked(fb.fil[0]['fo'] == 'min')
        self.ledOrderN.setText(str(fb.fil[0]['N']))
        self.ledOrderN.setEnabled(not self.chkMinOrder.isChecked()
                                  and status == 'a')
        self.lblOrderN.setEnabled(not self.chkMinOrder.isChecked()
                                  and status == 'a')

        if enb_signal:
            logger.debug("Emit 'filt_changed'")
            self.emit({'filt_changed': 'filter_type'})

# ------------------------------------------------------------------------------

    def _set_filter_order(self, enb_signal=False):
        """
        Triggered when either ledOrderN or chkMinOrder are edited:
        - copy settings to fb.fil[0]
        - emit 'filt_changed' if enb_signal is True
        """
        # Determine which subwidgets are _enabled_
        if self.chkMinOrder.isVisible():
            self.ledOrderN.setEnabled(not self.chkMinOrder.isChecked())
            self.lblOrderN.setEnabled(not self.chkMinOrder.isChecked())

            if self.chkMinOrder.isChecked() is True:
                # update in case N has been changed outside this class
                self.ledOrderN.setText(str(fb.fil[0]['N']))
                fb.fil[0].update({'fo': 'min'})

            else:
                fb.fil[0].update({'fo': 'man'})

        else:
            self.lblOrderN.setEnabled(self.fo == 'man')
            self.ledOrderN.setEnabled(self.fo == 'man')

        # read manual filter order, convert to positive integer and store it
        # in filter dictionary.
        ordn = safe_eval(self.ledOrderN.text(),
                         fb.fil[0]['N'],
                         return_type='int',
                         sign='pos')
        ordn = ordn if ordn > 0 else 1
        self.ledOrderN.setText(str(ordn))
        fb.fil[0].update({'N': ordn})

        if enb_signal:
            logger.debug("Emit 'filt_changed'")
            self.emit({'filt_changed': 'filter_order_widget'})

# ------------------------------------------------------------------------------

    def _destruct_dyn_widgets(self):
        """
        Delete the dynamically created filter design subwidget (if there is one)

        see http://stackoverflow.com/questions/13827798/proper-way-to-cleanup-
        widgets-in-pyqt

        This does NOT work when the subwidgets to be deleted and created are
        identical, as the deletion is only performed when the current scope has
        been left (?)! Hence, it is necessary to skip this method when the new
        design method is the same as the old one.
        """

        if hasattr(ff.fil_inst, 'wdg_fil'):
            # not needed, connection is destroyed automatically
            # ff.fil_inst.sig_tx.disconnect()
            try:
                # remove widget from layout
                self.layHDynWdg.removeWidget(self.dyn_wdg_fil)
                # delete UI widget when scope has been left
                self.dyn_wdg_fil.deleteLater()

            except AttributeError as e:
                logger.error("Could not destruct_UI!\n{0}".format(e))

            ff.fil_inst.deleteLater(
            )  # delete QWidget when scope has been left

# ------------------------------------------------------------------------------

    def _construct_dyn_widgets(self):
        """
        Create filter widget UI dynamically (if the filter routine has one) and
        connect its sig_tx signal to sig_tx in this scope.
        """
        ff.fil_inst.construct_UI()
        if hasattr(ff.fil_inst, 'wdg_fil'):
            try:
                self.dyn_wdg_fil = getattr(ff.fil_inst, 'wdg_fil')
                self.layHDynWdg.addWidget(self.dyn_wdg_fil, stretch=1)
            except AttributeError as e:
                logger.warning(e)

        if hasattr(ff.fil_inst, 'sig_tx'):
            ff.fil_inst.sig_tx.connect(self.sig_tx)
Exemplo n.º 11
0
class Plot_Tran_Stim_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming:
    sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)
    # outgoing: to fft related widgets (FFT window widget, qfft_win_select)
    sig_tx_fft = pyqtSignal(object)

    from pyfda.libs.pyfda_qt_lib import emit

    # ------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from
        - FFT window widget
        - qfft_win_select
        """

        logger.warning("PROCESS_SIG_RX - vis: {0}\n{1}".format(
            self.isVisible(), pprint_log(dict_sig)))

        if 'id' in dict_sig and dict_sig['id'] == id(self):
            logger.warning("Stopped infinite loop:\n{0}".format(
                pprint_log(dict_sig)))
            return
        elif 'view_changed' in dict_sig:
            if dict_sig['view_changed'] == 'f_S':
                self.recalc_freqs()

# ------------------------------------------------------------------------------

    def __init__(self):
        super().__init__()
        """
        Intitialize the widget, consisting of:
        - top chkbox row
        - coefficient table
        - two bottom rows with action buttons
        """
        # initial settings
        self.N_FFT = 0  # TODO: FFT value needs to be passed here somehow?

        # stimuli
        self.cmb_stim_item = "impulse"
        self.cmb_stim_periodic_item = 'square'
        self.stim = "dirac"
        self.impulse_type = 'dirac'
        self.sinusoid_type = 'sine'

        self.chirp_type = 'linear'
        self.modulation_type = 'am'
        self.noise = "None"

        self.f1 = 0.02
        self.f2 = 0.03
        self.A1 = 1.0
        self.A2 = 0.0
        self.phi1 = self.phi2 = 0
        self.T1 = self.T2 = 0
        self.TW1 = self.TW2 = 1
        self.BW1 = self.BW2 = 0.5
        self.noi = 0.1
        self.noise = 'none'
        self.DC = 0.0
        self.stim_formula = "A1 * abs(sin(2 * pi * f1 * n))"
        self.stim_par1 = 0.5

        self.scale_impz = 1  # optional energy scaling for impulses

        # self.bottom_f = -120  # initial value for log. scale
        # self.param = None

        # dictionaries with widgets needed for the various stimuli
        self.stim_wdg_dict = collections.OrderedDict()
        self.stim_wdg_dict.update({
            "none": {"dc", "noise"},
            "dirac": {"dc", "a1", "T1", "norm", "noise"},
            "sinc":
            {"dc", "a1", "a2", "T1", "T2", "f1", "f2", "norm", "noise"},
            "gauss": {
                "dc", "a1", "a2", "T1", "T2", "f1", "f2", "BW1", "BW2", "norm",
                "noise"
            },
            "rect": {"dc", "a1", "T1", "TW1", "norm", "noise"},
            "step": {"a1", "T1", "noise"},
            "cos": {"dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "noise"},
            "sine": {"dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "noise"},
            "exp": {"dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "noise"},
            "diric": {"dc", "a1", "phi1", "T1", "TW1", "f1", "noise"},
            "chirp": {"dc", "a1", "phi1", "f1", "f2", "T2", "noise"},
            "triang": {"dc", "a1", "phi1", "f1", "noise", "bl"},
            "saw": {"dc", "a1", "phi1", "f1", "noise", "bl"},
            "square": {"dc", "a1", "phi1", "f1", "noise", "bl", "par1"},
            "comb": {"dc", "a1", "phi1", "f1", "noise"},
            "am": {"dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "noise"},
            "pmfm": {"dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "noise"},
            "formula": {
                "dc", "a1", "a2", "phi1", "phi2", "f1", "f2", "BW1", "BW2",
                "noise"
            }
        })

        # combobox tooltip + data / text / tooltip for stimulus category items
        self.cmb_stim_items = [
            ("<span>Stimulus category.</span>"),
            ("none", "None",
             "<span>Only noise and DC can be selected.</span>"),
            ("impulse", "Impulse", "<span>Different impulses</span>"),
            ("step", "Step",
             "<span>Calculate step response and its error.</span>"),
            ("sinusoid", "Sinusoid", "<span>Sinusoidal waveforms</span>"),
            ("chirp", "Chirp", "<span>Different frequency sweeps.</span>"),
            ("periodic", "Periodic",
             "<span>Periodic functions with discontinuities, "
             "either band-limited or with aliasing.</span>"),
            ("modulation", "Modulat.", "<span>Modulated waveforms.</span>"),
            ("formula", "Formula", "<span>Formula defined stimulus.</span>")
        ]

        # combobox tooltip + data / text / tooltip for periodic signals items
        self.cmb_stim_periodic_items = [
            "<span>Periodic functions with discontinuities.</span>",
            ("square", "Square",
             "<span>Square signal with duty cycle &alpha;</span>"),
            ("saw", "Saw", "Sawtooth signal"),
            ("triang", "Triang", "Triangular signal"),
            ("comb", "Comb", "Comb signal")
        ]

        # combobox tooltip + data / text / tooltip for chirp signals items
        self.cmb_stim_chirp_items = [
            "<span>Type of frequency sweep from <i>f</i><sub>1</sub> @ <i>t</i> = 0 to "
            "<i>f</i><sub>2</sub> @ t = <i>T</i><sub>2</sub>.</span>",
            ("linear", "Lin", "Linear frequency sweep"),
            ("quadratic", "Square", "Quadratic frequency sweep"),
            ("logarithmic", "Log", "Logarithmic frequency sweep"),
            ("hyperbolic", "Hyper", "Hyperbolic frequency sweep")
        ]

        self.cmb_stim_impulse_items = [
            "<span>Different aperiodic impulse forms</span>",
            ("dirac", "Dirac",
             "<span>Discrete-time dirac impulse for simulating impulse and "
             "frequency response.</span>"),
            ("gauss", "Gauss",
             "<span>Gaussian pulse with bandpass spectrum and controllable "
             "relative -6 dB bandwidth.</span>"),
            ("sinc", "Sinc",
             "<span>Sinc pulse with rectangular baseband spectrum</span>"),
            ("rect", "Rect",
             "<span>Rectangular pulse with sinc-shaped spectrum</span>")
        ]

        self.cmb_stim_sinusoid_items = [
            "Sinusoidal or similar signals", ("sine", "Sine", "Sine signal"),
            ("cos", "Cos", "Cosine signal"),
            ("exp", "Exp", "Complex exponential"),
            ("diric", "Sinc",
             "<span>Periodic Sinc (Dirichlet function)</span>")
        ]

        # data / text / tooltip for noise stimulus combobox.
        self.cmb_stim_noise_items = [
            "Type of additive noise.", ("none", "None", ""),
            ("gauss", "Gauss",
             "<span>Normal- or Gauss-distributed process with std. deviation &sigma;."
             "</span>"),
            ("uniform", "Uniform",
             "<span>Uniformly distributed process in the range &plusmn; &Delta;/2."
             "</span>"),
            ("prbs", "PRBS",
             "<span>Pseudo-Random Binary Sequence with values &plusmn; A.</span>"
             ),
            ("mls", "MLS",
             "<span>Maximum Length Sequence with values &plusmn; A. The sequence is "
             "always the same as the state is not stored for the next sequence start."
             "</span>"),
            ("brownian", "Brownian",
             "<span>Brownian (cumulated sum) process based on Gaussian noise with"
             " std. deviation &sigma;.</span>")
        ]

        self._construct_UI()
        self._enable_stim_widgets()
        self._update_noi()

    def _construct_UI(self):
        # =====================================================================
        # Controls for stimuli
        # =====================================================================
        self.cmbStimulus = QComboBox(self)
        qcmb_box_populate(self.cmbStimulus, self.cmb_stim_items,
                          self.cmb_stim_item)

        self.lblStimPar1 = QLabel(to_html("&alpha; =", frmt='b'), self)
        self.ledStimPar1 = QLineEdit(self)
        self.ledStimPar1.setText("0.5")
        self.ledStimPar1.setToolTip("Duty Cycle, 0 ... 1")
        self.ledStimPar1.setObjectName("ledStimPar1")

        self.but_stim_bl = QPushButton(self)
        self.but_stim_bl.setText("BL")
        self.but_stim_bl.setToolTip(
            "<span>Bandlimit the signal to the Nyquist "
            "frequency to avoid aliasing. However, this is much slower "
            "to calculate especially for a large number of points.</span>")
        self.but_stim_bl.setMaximumWidth(qtext_width(text="BL "))
        self.but_stim_bl.setCheckable(True)
        self.but_stim_bl.setChecked(True)
        self.but_stim_bl.setObjectName("stim_bl")

        # -------------------------------------
        self.cmbChirpType = QComboBox(self)
        qcmb_box_populate(self.cmbChirpType, self.cmb_stim_chirp_items,
                          self.chirp_type)

        self.cmbImpulseType = QComboBox(self)
        qcmb_box_populate(self.cmbImpulseType, self.cmb_stim_impulse_items,
                          self.impulse_type)

        self.cmbSinusoidType = QComboBox(self)
        qcmb_box_populate(self.cmbSinusoidType, self.cmb_stim_sinusoid_items,
                          self.sinusoid_type)

        self.cmbPeriodicType = QComboBox(self)
        qcmb_box_populate(self.cmbPeriodicType, self.cmb_stim_periodic_items,
                          self.cmb_stim_periodic_item)

        self.cmbModulationType = QComboBox(self)
        for t in [("AM", "am"), ("PM / FM", "pmfm")]:  # text, data
            self.cmbModulationType.addItem(*t)
        qset_cmb_box(self.cmbModulationType, self.modulation_type, data=True)

        # -------------------------------------
        self.chk_step_err = QPushButton("Error", self)
        self.chk_step_err.setToolTip(
            "<span>Display the step response error.</span>")
        self.chk_step_err.setMaximumWidth(qtext_width(text="Error "))
        self.chk_step_err.setCheckable(True)
        self.chk_step_err.setChecked(False)
        self.chk_step_err.setObjectName("stim_step_err")

        layHCmbStim = QHBoxLayout()
        layHCmbStim.addWidget(self.cmbStimulus)
        layHCmbStim.addWidget(self.cmbImpulseType)
        layHCmbStim.addWidget(self.cmbSinusoidType)
        layHCmbStim.addWidget(self.cmbChirpType)
        layHCmbStim.addWidget(self.cmbPeriodicType)
        layHCmbStim.addWidget(self.cmbModulationType)
        layHCmbStim.addWidget(self.but_stim_bl)
        layHCmbStim.addWidget(self.lblStimPar1)
        layHCmbStim.addWidget(self.ledStimPar1)
        layHCmbStim.addWidget(self.chk_step_err)

        self.lblDC = QLabel(to_html("DC =", frmt='bi'), self)
        self.ledDC = QLineEdit(self)
        self.ledDC.setText(str(self.DC))
        self.ledDC.setToolTip("DC Level")
        self.ledDC.setObjectName("stimDC")

        layHStimDC = QHBoxLayout()
        layHStimDC.addWidget(self.lblDC)
        layHStimDC.addWidget(self.ledDC)

        # ======================================================================
        self.lblAmp1 = QLabel(to_html("&nbsp;A_1", frmt='bi') + " =", self)
        self.ledAmp1 = QLineEdit(self)
        self.ledAmp1.setText(str(self.A1))
        self.ledAmp1.setToolTip(
            "Stimulus amplitude, complex values like 3j - 1 are allowed")
        self.ledAmp1.setObjectName("stimAmp1")

        self.lblAmp2 = QLabel(to_html("&nbsp;A_2", frmt='bi') + " =", self)
        self.ledAmp2 = QLineEdit(self)
        self.ledAmp2.setText(str(self.A2))
        self.ledAmp2.setToolTip(
            "Stimulus amplitude 2, complex values like 3j - 1 are allowed")
        self.ledAmp2.setObjectName("stimAmp2")
        # ----------------------------------------------
        self.lblPhi1 = QLabel(to_html("&nbsp;&phi;_1", frmt='bi') + " =", self)
        self.ledPhi1 = QLineEdit(self)
        self.ledPhi1.setText(str(self.phi1))
        self.ledPhi1.setToolTip("Stimulus phase")
        self.ledPhi1.setObjectName("stimPhi1")
        self.lblPhU1 = QLabel(to_html("&deg;", frmt='b'), self)

        self.lblPhi2 = QLabel(to_html("&nbsp;&phi;_2", frmt='bi') + " =", self)
        self.ledPhi2 = QLineEdit(self)
        self.ledPhi2.setText(str(self.phi2))
        self.ledPhi2.setToolTip("Stimulus phase 2")
        self.ledPhi2.setObjectName("stimPhi2")
        self.lblPhU2 = QLabel(to_html("&deg;", frmt='b'), self)
        # ----------------------------------------------
        self.lbl_T1 = QLabel(to_html("&nbsp;T_1", frmt='bi') + " =", self)
        self.led_T1 = QLineEdit(self)
        self.led_T1.setText(str(self.T1))
        self.led_T1.setToolTip("Time shift")
        self.led_T1.setObjectName("stimT1")
        self.lbl_TU1 = QLabel(to_html("T_S", frmt='b'), self)

        self.lbl_T2 = QLabel(to_html("&nbsp;T_2", frmt='bi') + " =", self)
        self.led_T2 = QLineEdit(self)
        self.led_T2.setText(str(self.T2))
        self.led_T2.setToolTip("Time shift 2")
        self.led_T2.setObjectName("stimT2")
        self.lbl_TU2 = QLabel(to_html("T_S", frmt='b'), self)
        # ---------------------------------------------
        self.lbl_TW1 = QLabel(
            to_html("&nbsp;&Delta;T_1", frmt='bi') + " =", self)
        self.led_TW1 = QLineEdit(self)
        self.led_TW1.setText(str(self.TW1))
        self.led_TW1.setToolTip("Time width")
        self.led_TW1.setObjectName("stimTW1")
        self.lbl_TWU1 = QLabel(to_html("T_S", frmt='b'), self)

        self.lbl_TW2 = QLabel(
            to_html("&nbsp;&Delta;T_2", frmt='bi') + " =", self)
        self.led_TW2 = QLineEdit(self)
        self.led_TW2.setText(str(self.TW2))
        self.led_TW2.setToolTip("Time width 2")
        self.led_TW2.setObjectName("stimTW2")
        self.lbl_TWU2 = QLabel(to_html("T_S", frmt='b'), self)
        # ----------------------------------------------
        self.txtFreq1_f = to_html("&nbsp;f_1", frmt='bi') + " ="
        self.txtFreq1_k = to_html("&nbsp;k_1", frmt='bi') + " ="
        self.lblFreq1 = QLabel(self.txtFreq1_f, self)
        self.ledFreq1 = QLineEdit(self)
        self.ledFreq1.setText(str(self.f1))
        self.ledFreq1.setToolTip("Stimulus frequency")
        self.ledFreq1.setObjectName("stimFreq1")
        self.lblFreqUnit1 = QLabel("f_S", self)

        self.txtFreq2_f = to_html("&nbsp;f_2", frmt='bi') + " ="
        self.txtFreq2_k = to_html("&nbsp;k_2", frmt='bi') + " ="
        self.lblFreq2 = QLabel(self.txtFreq2_f, self)
        self.ledFreq2 = QLineEdit(self)
        self.ledFreq2.setText(str(self.f2))
        self.ledFreq2.setToolTip("Stimulus frequency 2")
        self.ledFreq2.setObjectName("stimFreq2")
        self.lblFreqUnit2 = QLabel("f_S", self)
        # ----------------------------------------------
        self.lbl_BW1 = QLabel(
            to_html(self.tr("&nbsp;BW_1"), frmt='bi') + " =", self)
        self.led_BW1 = QLineEdit(self)
        self.led_BW1.setText(str(self.BW1))
        self.led_BW1.setToolTip(self.tr("Relative bandwidth"))
        self.led_BW1.setObjectName("stimBW1")

        self.lbl_BW2 = QLabel(
            to_html(self.tr("&nbsp;BW_2"), frmt='bi') + " =", self)
        self.led_BW2 = QLineEdit(self)
        self.led_BW2.setText(str(self.BW2))
        self.led_BW2.setToolTip(self.tr("Relative bandwidth 2"))
        self.led_BW2.setObjectName("stimBW2")
        # ----------------------------------------------
        self.lblNoise = QLabel(to_html("&nbsp;Noise", frmt='bi'), self)
        self.cmbNoise = QComboBox(self)
        qcmb_box_populate(self.cmbNoise, self.cmb_stim_noise_items, self.noise)

        self.lblNoi = QLabel("not initialized", self)
        self.ledNoi = QLineEdit(self)
        self.ledNoi.setText(str(self.noi))
        self.ledNoi.setToolTip("not initialized")
        self.ledNoi.setObjectName("stimNoi")

        layGStim = QGridLayout()

        layGStim.addLayout(layHCmbStim, 0, 1)
        layGStim.addLayout(layHStimDC, 1, 1)

        layGStim.addWidget(self.lblAmp1, 0, 2)
        layGStim.addWidget(self.lblAmp2, 1, 2)

        layGStim.addWidget(self.ledAmp1, 0, 3)
        layGStim.addWidget(self.ledAmp2, 1, 3)

        layGStim.addWidget(self.lblPhi1, 0, 4)
        layGStim.addWidget(self.lblPhi2, 1, 4)

        layGStim.addWidget(self.ledPhi1, 0, 5)
        layGStim.addWidget(self.ledPhi2, 1, 5)

        layGStim.addWidget(self.lblPhU1, 0, 6)
        layGStim.addWidget(self.lblPhU2, 1, 6)

        layGStim.addWidget(self.lbl_T1, 0, 7)
        layGStim.addWidget(self.lbl_T2, 1, 7)

        layGStim.addWidget(self.led_T1, 0, 8)
        layGStim.addWidget(self.led_T2, 1, 8)

        layGStim.addWidget(self.lbl_TU1, 0, 9)
        layGStim.addWidget(self.lbl_TU2, 1, 9)

        layGStim.addWidget(self.lbl_TW1, 0, 10)
        layGStim.addWidget(self.lbl_TW2, 1, 10)

        layGStim.addWidget(self.led_TW1, 0, 11)
        layGStim.addWidget(self.led_TW2, 1, 11)

        layGStim.addWidget(self.lbl_TWU1, 0, 12)
        layGStim.addWidget(self.lbl_TWU2, 1, 12)

        layGStim.addWidget(self.lblFreq1, 0, 13)
        layGStim.addWidget(self.lblFreq2, 1, 13)

        layGStim.addWidget(self.ledFreq1, 0, 14)
        layGStim.addWidget(self.ledFreq2, 1, 14)

        layGStim.addWidget(self.lblFreqUnit1, 0, 15)
        layGStim.addWidget(self.lblFreqUnit2, 1, 15)

        layGStim.addWidget(self.lbl_BW1, 0, 16)
        layGStim.addWidget(self.lbl_BW2, 1, 16)

        layGStim.addWidget(self.led_BW1, 0, 17)
        layGStim.addWidget(self.led_BW2, 1, 17)

        layGStim.addWidget(self.lblNoise, 0, 18)
        layGStim.addWidget(self.lblNoi, 1, 18)

        layGStim.addWidget(self.cmbNoise, 0, 19)
        layGStim.addWidget(self.ledNoi, 1, 19)

        # ----------------------------------------------
        self.lblStimFormula = QLabel(to_html("x =", frmt='bi'), self)
        self.ledStimFormula = QLineEdit(self)
        self.ledStimFormula.setText(str(self.stim_formula))
        self.ledStimFormula.setToolTip(
            "<span>Enter formula for stimulus in numexpr syntax.</span>")
        self.ledStimFormula.setObjectName("stimFormula")

        layH_stim_formula = QHBoxLayout()
        layH_stim_formula.addWidget(self.lblStimFormula)
        layH_stim_formula.addWidget(self.ledStimFormula, 10)

        # ----------------------------------------------------------------------
        # Main Widget
        # ----------------------------------------------------------------------
        layH_stim_par = QHBoxLayout()
        layH_stim_par.addLayout(layGStim)

        layV_stim = QVBoxLayout()
        layV_stim.addLayout(layH_stim_par)
        layV_stim.addLayout(layH_stim_formula)

        layH_stim = QHBoxLayout()
        layH_stim.addLayout(layV_stim)
        layH_stim.addStretch(10)

        self.wdg_stim = QWidget(self)
        self.wdg_stim.setLayout(layH_stim)
        self.wdg_stim.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)

        # ----------------------------------------------------------------------
        # Event Filter
        # ----------------------------------------------------------------------
        # frequency related widgets are scaled with f_s, requiring special handling
        self.ledFreq1.installEventFilter(self)
        self.ledFreq2.installEventFilter(self)
        self.led_T1.installEventFilter(self)
        self.led_T2.installEventFilter(self)
        self.led_TW1.installEventFilter(self)
        self.led_TW2.installEventFilter(self)

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        # --- stimulus control ---
        self.but_stim_bl.clicked.connect(self._enable_stim_widgets)
        self.chk_step_err.clicked.connect(self._enable_stim_widgets)
        self.cmbStimulus.currentIndexChanged.connect(self._enable_stim_widgets)

        self.cmbNoise.currentIndexChanged.connect(self._update_noi)
        self.ledNoi.editingFinished.connect(self._update_noi)
        self.ledAmp1.editingFinished.connect(self._update_amp1)
        self.ledAmp2.editingFinished.connect(self._update_amp2)
        self.ledPhi1.editingFinished.connect(self._update_phi1)
        self.ledPhi2.editingFinished.connect(self._update_phi2)
        self.led_BW1.editingFinished.connect(self._update_BW1)
        self.led_BW2.editingFinished.connect(self._update_BW2)

        self.cmbImpulseType.currentIndexChanged.connect(
            self._update_impulse_type)
        self.cmbSinusoidType.currentIndexChanged.connect(
            self._update_sinusoid_type)
        self.cmbChirpType.currentIndexChanged.connect(self._update_chirp_type)
        self.cmbPeriodicType.currentIndexChanged.connect(
            self._update_periodic_type)
        self.cmbModulationType.currentIndexChanged.connect(
            self._update_modulation_type)

        self.ledDC.editingFinished.connect(self._update_DC)
        self.ledStimFormula.editingFinished.connect(self._update_stim_formula)
        self.ledStimPar1.editingFinished.connect(self._update_stim_par1)

# ------------------------------------------------------------------------------

    def update_freq_units(self):
        """
        Update labels referrring to frequency specs
        """

        if fb.fil[0]['freq_specs_unit'] == 'k':
            f_unit = ''
            t_unit = ''
            self.lblFreq1.setText(self.txtFreq1_k)
            self.lblFreq2.setText(self.txtFreq2_k)
        else:
            f_unit = fb.fil[0]['plt_fUnit']
            t_unit = fb.fil[0]['plt_tUnit'].replace(r"$\mu$", "&mu;")
            self.lblFreq1.setText(self.txtFreq1_f)
            self.lblFreq2.setText(self.txtFreq2_f)

        if f_unit in {"f_S", "f_Ny"}:
            unit_frmt = "i"  # italic
        else:
            unit_frmt = None  # don't print units like kHz in italic

        self.lblFreqUnit1.setText(to_html(f_unit, frmt=unit_frmt))
        self.lblFreqUnit2.setText(to_html(f_unit, frmt=unit_frmt))
        self.lbl_TU1.setText(to_html(t_unit, frmt=unit_frmt))
        self.lbl_TU2.setText(to_html(t_unit, frmt=unit_frmt))

# ------------------------------------------------------------------------------

    def eventFilter(self, source, event):
        """
        Filter all events generated by the monitored widgets (ledFreq1 and 2 and T1 / T2).
        Source and type of all events generated by monitored objects are passed
         to this eventFilter, evaluated and passed on to the next hierarchy level.

        - When a QLineEdit widget gains input focus (``QEvent.FocusIn``), display
          the stored value from filter dict with full precision
        - When a key is pressed inside the text field, set the `spec_edited` flag
          to True.
        - When a QLineEdit widget loses input focus (``QEvent.FocusOut``), store
          current value normalized to f_S with full precision (only if
          ``spec_edited == True``) and display the stored value in selected format

          Emit 'ui_changed':'stim'
        """
        def _reload_entry(source):
            """ Reload text entry for active line edit field in rounded format """
            if source.objectName() == "stimFreq1":
                source.setText(
                    str(params['FMT'].format(self.f1 * self.f_scale)))
            elif source.objectName() == "stimFreq2":
                source.setText(
                    str(params['FMT'].format(self.f2 * self.f_scale)))
            elif source.objectName() == "stimT1":
                source.setText(
                    str(params['FMT'].format(self.T1 * self.t_scale)))
            elif source.objectName() == "stimT2":
                source.setText(
                    str(params['FMT'].format(self.T2 * self.t_scale)))
            elif source.objectName() == "stimTW1":
                source.setText(
                    str(params['FMT'].format(self.TW1 * self.t_scale)))
            elif source.objectName() == "stimTW2":
                source.setText(
                    str(params['FMT'].format(self.TW2 * self.t_scale)))

        def _store_entry(source):
            if self.spec_edited:
                if source.objectName() == "stimFreq1":
                    self.f1 = safe_eval(source.text(),
                                        self.f1 * self.f_scale,
                                        return_type='float') / self.f_scale
                    source.setText(
                        str(params['FMT'].format(self.f1 * self.f_scale)))

                elif source.objectName() == "stimFreq2":
                    self.f2 = safe_eval(source.text(),
                                        self.f2 * self.f_scale,
                                        return_type='float') / self.f_scale
                    source.setText(
                        str(params['FMT'].format(self.f2 * self.f_scale)))

                elif source.objectName() == "stimT1":
                    self.T1 = safe_eval(source.text(),
                                        self.T1 * self.t_scale,
                                        return_type='float') / self.t_scale
                    source.setText(
                        str(params['FMT'].format(self.T1 * self.t_scale)))

                elif source.objectName() == "stimT2":
                    self.T2 = safe_eval(source.text(),
                                        self.T2 * self.t_scale,
                                        return_type='float') / self.t_scale
                    source.setText(
                        str(params['FMT'].format(self.T2 * self.t_scale)))

                elif source.objectName() == "stimTW1":
                    self.TW1 = safe_eval(source.text(),
                                         self.TW1 * self.t_scale,
                                         sign='pos',
                                         return_type='float') / self.t_scale
                    source.setText(
                        str(params['FMT'].format(self.TW1 * self.t_scale)))

                elif source.objectName() == "stimTW2":
                    self.TW2 = safe_eval(source.text(),
                                         self.TW2 * self.t_scale,
                                         sign='pos',
                                         return_type='float') / self.t_scale
                    source.setText(
                        str(params['FMT'].format(self.TW2 * self.t_scale)))

                self.spec_edited = False  # reset flag
                self._update_scale_impz()
                self.emit({'ui_changed': 'stim'})

            # nothing has changed, but display frequencies in rounded format anyway
            else:
                _reload_entry(source)

        # --------------------------------------------------------------------

#        if isinstance(source, QLineEdit):
#        if source.objectName() in {"stimFreq1","stimFreq2"}:
        if event.type() in {QEvent.FocusIn, QEvent.KeyPress, QEvent.FocusOut}:
            if event.type() == QEvent.FocusIn:
                self.spec_edited = False
                self.update_freqs()
            elif event.type() == QEvent.KeyPress:
                self.spec_edited = True  # entry has been changed
                key = event.key()
                if key in {Qt.Key_Return, Qt.Key_Enter}:
                    _store_entry(source)
                elif key == Qt.Key_Escape:  # revert changes
                    self.spec_edited = False
                    _reload_entry(source)

            elif event.type() == QEvent.FocusOut:
                _store_entry(source)

        # Call base class method to continue normal event processing:
        return super(Plot_Tran_Stim_UI, self).eventFilter(source, event)

    # -------------------------------------------------------------
    def recalc_freqs(self):
        """
        Update normalized frequencies if required. This is called by via signal
        ['ui_changed':'f_S'] from plot_impz.process_sig_rx
        """
        if fb.fil[0]['freq_locked']:
            self.f1 *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']
            self.f2 *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']
            self.T1 *= fb.fil[0]['f_S'] / fb.fil[0]['f_S_prev']
            self.T2 *= fb.fil[0]['f_S'] / fb.fil[0]['f_S_prev']
            self.TW1 *= fb.fil[0]['f_S'] / fb.fil[0]['f_S_prev']
            self.TW2 *= fb.fil[0]['f_S'] / fb.fil[0]['f_S_prev']

        self._update_scale_impz()

        self.update_freqs()

        self.emit({'ui_changed': 'f1_f2'})

    # -------------------------------------------------------------
    def update_freqs(self):
        """
        `update_freqs()` is called:

        - when one of the stimulus frequencies has changed via eventFilter()
        - sampling frequency has been changed via signal ['ui_changed':'f_S']
          from plot_impz.process_sig_rx -> self.recalc_freqs

        The sampling frequency is loaded from filter dictionary and stored as
        `self.f_scale` (except when the frequency unit is k when `f_scale = self.N_FFT`).

        Frequency field entries are always stored normalized w.r.t. f_S in the
        dictionary: When the `f_S` lock button is unlocked, only the displayed
        values for frequency entries are updated with f_S, not the dictionary.

        When the `f_S` lock button is pressed, the absolute frequency values in
        the widget fields are kept constant, and the dictionary entries are updated.

        """

        # recalculate displayed freq spec values for (maybe) changed f_S
        if fb.fil[0]['freq_specs_unit'] == 'k':
            self.f_scale = self.N_FFT
        else:
            self.f_scale = fb.fil[0]['f_S']
        self.t_scale = fb.fil[0]['T_S']

        if self.ledFreq1.hasFocus():
            # widget has focus, show full precision
            self.ledFreq1.setText(str(self.f1 * self.f_scale))

        elif self.ledFreq2.hasFocus():
            self.ledFreq2.setText(str(self.f2 * self.f_scale))

        elif self.led_T1.hasFocus():
            self.led_T1.setText(str(self.T1 * self.t_scale))

        elif self.led_T2.hasFocus():
            self.led_T2.setText(str(self.T2 * self.t_scale))

        elif self.led_TW1.hasFocus():
            self.led_TW1.setText(str(self.TW1 * self.t_scale))

        elif self.led_TW2.hasFocus():
            self.led_TW2.setText(str(self.TW2 * self.t_scale))

        else:
            # widgets have no focus, round the display
            self.ledFreq1.setText(
                str(params['FMT'].format(self.f1 * self.f_scale)))
            self.ledFreq2.setText(
                str(params['FMT'].format(self.f2 * self.f_scale)))
            self.led_T1.setText(
                str(params['FMT'].format(self.T1 * self.t_scale)))
            self.led_T2.setText(
                str(params['FMT'].format(self.T2 * self.t_scale)))
            self.led_TW1.setText(
                str(params['FMT'].format(self.TW1 * self.t_scale)))
            self.led_TW2.setText(
                str(params['FMT'].format(self.TW2 * self.t_scale)))

        self.update_freq_units(
        )  # TODO: should only be called at f_S / unit update

    # -------------------------------------------------------------
    def _enable_stim_widgets(self):
        """ Enable / disable widgets depending on the selected stimulus """
        self.cmb_stim = qget_cmb_box(self.cmbStimulus)
        if self.cmb_stim == "impulse":
            self.stim = qget_cmb_box(self.cmbImpulseType)
            # recalculate the energy scaling for impulse functions
            self._update_scale_impz()

        elif self.cmb_stim == "sinusoid":
            self.stim = qget_cmb_box(self.cmbSinusoidType)
        elif self.cmb_stim == "periodic":
            self.stim = qget_cmb_box(self.cmbPeriodicType)
        elif self.cmb_stim == "modulation":
            self.stim = qget_cmb_box(self.cmbModulationType)
        else:
            self.stim = self.cmb_stim

        # read out which stimulus widgets are enabled
        stim_wdg = self.stim_wdg_dict[self.stim]

        self.lblDC.setVisible("dc" in stim_wdg)
        self.ledDC.setVisible("dc" in stim_wdg)

        self.chk_step_err.setVisible(self.stim == "step")

        self.lblStimPar1.setVisible("par1" in stim_wdg)
        self.ledStimPar1.setVisible("par1" in stim_wdg)

        self.but_stim_bl.setVisible("bl" in stim_wdg)

        self.lblAmp1.setVisible("a1" in stim_wdg)
        self.ledAmp1.setVisible("a1" in stim_wdg)
        self.lblPhi1.setVisible("phi1" in stim_wdg)
        self.ledPhi1.setVisible("phi1" in stim_wdg)
        self.lblPhU1.setVisible("phi1" in stim_wdg)
        self.lbl_T1.setVisible("T1" in stim_wdg)
        self.led_T1.setVisible("T1" in stim_wdg)
        self.lbl_TU1.setVisible("T1" in stim_wdg)
        self.lbl_TW1.setVisible("TW1" in stim_wdg)
        self.led_TW1.setVisible("TW1" in stim_wdg)
        self.lbl_TWU1.setVisible("TW1" in stim_wdg)
        self.lblFreq1.setVisible("f1" in stim_wdg)
        self.ledFreq1.setVisible("f1" in stim_wdg)
        self.lblFreqUnit1.setVisible("f1" in stim_wdg)
        self.lbl_BW1.setVisible("BW1" in stim_wdg)
        self.led_BW1.setVisible("BW1" in stim_wdg)

        self.lblAmp2.setVisible("a2" in stim_wdg)
        self.ledAmp2.setVisible("a2" in stim_wdg)
        self.lblPhi2.setVisible("phi2" in stim_wdg)
        self.ledPhi2.setVisible("phi2" in stim_wdg)
        self.lblPhU2.setVisible("phi2" in stim_wdg)
        self.lbl_T2.setVisible("T2" in stim_wdg)
        self.led_T2.setVisible("T2" in stim_wdg)
        self.lbl_TU2.setVisible("T2" in stim_wdg)
        self.lbl_TW2.setVisible("TW2" in stim_wdg)
        self.led_TW2.setVisible("TW2" in stim_wdg)
        self.lbl_TWU2.setVisible("TW2" in stim_wdg)
        self.lblFreq2.setVisible("f2" in stim_wdg)
        self.ledFreq2.setVisible("f2" in stim_wdg)
        self.lblFreqUnit2.setVisible("f2" in stim_wdg)
        self.lbl_BW2.setVisible("BW2" in stim_wdg)
        self.led_BW2.setVisible("BW2" in stim_wdg)
        self.lblStimFormula.setVisible(self.stim == "formula")
        self.ledStimFormula.setVisible(self.stim == "formula")

        self.cmbImpulseType.setVisible(self.cmb_stim == 'impulse')
        self.cmbSinusoidType.setVisible(self.cmb_stim == 'sinusoid')
        self.cmbChirpType.setVisible(self.cmb_stim == 'chirp')
        self.cmbPeriodicType.setVisible(self.cmb_stim == 'periodic')
        self.cmbModulationType.setVisible(self.cmb_stim == 'modulation')

        self.emit({'ui_changed': 'stim'})

    # -------------------------------------------------------------
    def _update_amp1(self):
        """ Update value for self.A1 from QLineEditWidget"""
        self.A1 = safe_eval(self.ledAmp1.text(), self.A1, return_type='cmplx')
        self.ledAmp1.setText(str(self.A1))
        self.emit({'ui_changed': 'a1'})

    def _update_amp2(self):
        """ Update value for self.A2 from the QLineEditWidget"""
        self.A2 = safe_eval(self.ledAmp2.text(), self.A2, return_type='cmplx')
        self.ledAmp2.setText(str(self.A2))
        self.emit({'ui_changed': 'a2'})

    def _update_phi1(self):
        """ Update value for self.phi1 from QLineEditWidget"""
        self.phi1 = safe_eval(self.ledPhi1.text(),
                              self.phi1,
                              return_type='float')
        self.ledPhi1.setText(str(self.phi1))
        self.emit({'ui_changed': 'phi1'})

    def _update_BW1(self):
        """ Update value for self.BW1 from QLineEditWidget"""
        self.BW1 = safe_eval(self.led_BW1.text(),
                             self.BW1,
                             return_type='float',
                             sign='pos')
        self.led_BW1.setText(str(self.BW1))
        self._update_scale_impz()
        self.emit({'ui_changed': 'BW1'})

    def _update_BW2(self):
        """ Update value for self.BW2 from QLineEditWidget"""
        self.BW2 = safe_eval(self.led_BW2.text(),
                             self.BW2,
                             return_type='float',
                             sign='pos')
        self.led_BW2.setText(str(self.BW2))
        self.emit({'ui_changed': 'BW2'})

    def _update_scale_impz(self):
        """
        recalculate the energy scaling for impulse functions when impulse type or
        relevant frequency / bandwidth parameter have been updated
        """
        if self.stim == "dirac":
            self.scale_impz = 1.
        elif self.stim == "sinc":
            self.scale_impz = self.f1 * 2
        elif self.stim == "gauss":
            self.scale_impz = self.f1 * 2 * self.BW1
        elif self.stim == "rect":
            self.scale_impz = 1. / self.TW1

    def _update_phi2(self):
        """ Update value for self.phi2 from the QLineEditWidget"""
        self.phi2 = safe_eval(self.ledPhi2.text(),
                              self.phi2,
                              return_type='float')
        self.ledPhi2.setText(str(self.phi2))
        self.emit({'ui_changed': 'phi2'})

    def _update_chirp_type(self):
        """ Update value for self.chirp_type from data field of ComboBox"""
        self.chirp_type = qget_cmb_box(self.cmbChirpType)
        self.emit({'ui_changed': 'chirp_type'})

    def _update_impulse_type(self):
        """ Update value for self.impulse_type from data field of ComboBox"""
        self.impulse_type = qget_cmb_box(self.cmbImpulseType)
        self._enable_stim_widgets()

    def _update_sinusoid_type(self):
        """ Update value for self.sinusoid_type from data field of ComboBox"""
        self.sinusoid_type = qget_cmb_box(self.cmbSinusoidType)
        self._enable_stim_widgets()

    def _update_periodic_type(self):
        """ Update value for self.periodic_type from data field of ComboBox"""
        self.periodic_type = qget_cmb_box(self.cmbPeriodicType)
        self._enable_stim_widgets()

    def _update_modulation_type(self):
        """ Update value for self.modulation_type from from data field of ComboBox"""
        self.modulation_type = qget_cmb_box(self.cmbModulationType)
        self._enable_stim_widgets()

    # -------------------------------------------------------------
    def _update_noi(self):
        """ Update type + value + label for self.noi for noise"""
        self.noise = qget_cmb_box(self.cmbNoise)
        self.lblNoi.setVisible(self.noise != 'none')
        self.ledNoi.setVisible(self.noise != 'none')
        if self.noise != 'none':
            self.noi = safe_eval(self.ledNoi.text(), 0, return_type='cmplx')
            self.ledNoi.setText(str(self.noi))
            if self.noise == 'gauss':
                self.lblNoi.setText(to_html("&nbsp;&sigma; =", frmt='bi'))
                self.ledNoi.setToolTip(
                    "<span>Standard deviation of statistical process,"
                    "noise power is <i>P</i> = &sigma;<sup>2</sup></span>")
            elif self.noise == 'uniform':
                self.lblNoi.setText(to_html("&nbsp;&Delta; =", frmt='bi'))
                self.ledNoi.setToolTip(
                    "<span>Interval size for uniformly distributed process (e.g. "
                    "quantization step size for quantization noise), centered around 0. "
                    "Noise power is <i>P</i> = &Delta;<sup>2</sup>/12.</span>")
            elif self.noise == 'prbs':
                self.lblNoi.setText(to_html("&nbsp;A =", frmt='bi'))
                self.ledNoi.setToolTip(
                    "<span>Amplitude of bipolar Pseudorandom Binary Sequence. "
                    "Noise power is <i>P</i> = A<sup>2</sup>.</span>")

            elif self.noise == 'mls':
                self.lblNoi.setText(to_html("&nbsp;A =", frmt='bi'))
                self.ledNoi.setToolTip(
                    "<span>Amplitude of Maximum Length Sequence. "
                    "Noise power is <i>P</i> = A<sup>2</sup>.</span>")
            elif self.noise == 'brownian':
                self.lblNoi.setText(to_html("&nbsp;&sigma; =", frmt='bi'))
                self.ledNoi.setToolTip(
                    "<span>Standard deviation of the Gaussian process "
                    "that is cumulated.</span>")

        self.emit({'ui_changed': 'noi'})

    def _update_DC(self):
        """ Update value for self.DC from the QLineEditWidget"""
        self.DC = safe_eval(self.ledDC.text(), 0, return_type='cmplx')
        self.ledDC.setText(str(self.DC))
        self.emit({'ui_changed': 'dc'})

    def _update_stim_formula(self):
        """Update string with formula to be evaluated by numexpr"""
        self.stim_formula = self.ledStimFormula.text().strip()
        self.ledStimFormula.setText(str(self.stim_formula))
        self.emit({'ui_changed': 'stim_formula'})

    def _update_stim_par1(self):
        """ Update value for self.par1 from QLineEditWidget"""
        self.stim_par1 = safe_eval(self.ledStimPar1.text(),
                                   self.stim_par1,
                                   sign='pos',
                                   return_type='float')
        self.ledStimPar1.setText(str(self.stim_par1))
        self.emit({'ui_changed': 'stim_par1'})
Exemplo n.º 12
0
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
Exemplo n.º 13
0
class MA(QWidget):

    FRMT = (
        'zpk', 'ba'
    )  # output format(s) of filter design routines 'zpk' / 'ba' / 'sos'

    info = """
**Moving average filters**

can only be specified via their length and the number of cascaded sections.

The minimum order to obtain a certain attenuation at a given frequency is
calculated via the si function.

Moving average filters can be implemented very efficiently in hard- and software
as they require no multiplications but only addition and subtractions. Probably
only the lowpass is really useful, as the other response types only filter out resp.
leave components at ``f_S/4`` (bandstop resp. bandpass) resp. leave components
near ``f_S/2`` (highpass).

**Design routines:**

``ma.calc_ma()``
    """
    sig_tx = pyqtSignal(object)
    from pyfda.libs.pyfda_qt_lib import emit

    def __init__(self):
        QWidget.__init__(self)

        self.delays = 12  # number of delays per stage
        self.stages = 1  # number of stages

        self.ft = 'FIR'

        self.rt_dicts = ()
        # Common data for all filter response types:
        # This data is merged with the entries for individual response types
        # (common data comes first):

        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "Enter desired order (= delays) <b><i>M</i></b> per stage and"
                     " the number of <b>stages</b>. Target frequencies and amplitudes"
                     " are only used for comparison, not for the design itself."
                     )
                },
                'min': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "Enter desired attenuation <b><i>A<sub>SB</sub></i></b> at "
                     "the corner of the stop band <b><i>F<sub>SB</sub></i></b>. "
                     "Choose the number of <b>stages</b>, the minimum order <b><i>M</i></b> "
                     "per stage will be determined. Passband specs are not regarded."
                     )
                }
            },
            'LP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_PB', 'F_SB'),
                        'amp': ('u', 'A_PB', 'A_SB')
                    })
                },
                'min': {
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('a', 'A_PB', 'A_SB')
                    })
                }
            },
            'HP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_SB', 'F_PB'),
                        'amp': ('u', 'A_SB', 'A_PB')
                    })
                },
                'min': {
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('a', 'A_SB', 'A_PB')
                    })
                },
            },
            'BS': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('u', 'A_PB', 'A_SB', 'A_PB2')
                    }),
                    'msg':
                    ('a', "\nThis is not a proper band stop, it only lets pass"
                     " frequency components around DC and <i>f<sub>S</sub></i>/2."
                     " The order needs to be odd."),
                }
            },
            'BP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': (
                            'u',
                            'F_SB',
                            'F_PB',
                            'F_PB2',
                            'F_SB2',
                        ),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    }),
                    'msg':
                    ('a', "\nThis is not a proper band pass, it only lets pass"
                     " frequency components around <i>f<sub>S</sub></i>/4."
                     " The order needs to be odd."),
                }
            },
        }

        self.info_doc = []
#        self.info_doc.append('remez()\n=======')
#        self.info_doc.append(sig.remez.__doc__)
#        self.info_doc.append('remezord()\n==========')
#        self.info_doc.append(remezord.__doc__)

#--------------------------------------------------------------------------

    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """

        self.lbl_delays = QLabel("<b><i>M =</ i></ b>", self)
        self.lbl_delays.setObjectName('wdg_lbl_ma_0')
        self.led_delays = QLineEdit(self)
        try:
            self.led_delays.setText(str(fb.fil[0]['N']))
        except KeyError:
            self.led_delays.setText(str(self.delays))
        self.led_delays.setObjectName('wdg_led_ma_0')
        self.led_delays.setToolTip("Set number of delays per stage")

        self.lbl_stages = QLabel("<b>Stages =</ b>", self)
        self.lbl_stages.setObjectName('wdg_lbl_ma_1')
        self.led_stages = QLineEdit(self)
        self.led_stages.setText(str(self.stages))

        self.led_stages.setObjectName('wdg_led_ma_1')
        self.led_stages.setToolTip("Set number of stages ")

        self.chk_norm = QCheckBox("Normalize", self)
        self.chk_norm.setChecked(True)
        self.chk_norm.setObjectName('wdg_chk_ma_2')
        self.chk_norm.setToolTip("Normalize to| H_max = 1|")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_delays)
        self.layHWin.addWidget(self.led_delays)
        self.layHWin.addStretch(1)
        self.layHWin.addWidget(self.lbl_stages)
        self.layHWin.addWidget(self.led_stages)
        self.layHWin.addStretch(1)
        self.layHWin.addWidget(self.chk_norm)
        self.layHWin.setContentsMargins(0, 0, 0, 0)
        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.led_delays.editingFinished.connect(self._update_UI)
        self.led_stages.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        self.chk_norm.clicked.connect(self._update_UI)
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _load_dict(self):
        """
        Reload parameter(s) from filter dictionary (if they exist) and set
        corresponding UI elements. load_dict() is called upon initialization
        and when the filter is loaded from disk.
        """
        if 'wdg_fil' in fb.fil[0] and 'ma' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['ma']
            if 'delays' in wdg_fil_par:
                self.delays = wdg_fil_par['delays']
                self.led_delays.setText(str(self.delays))
            if 'stages' in wdg_fil_par:
                self.stages = wdg_fil_par['stages']
                self.led_stages.setText(str(self.stages))
            if 'normalize' in wdg_fil_par:
                self.chk_norm.setChecked(wdg_fil_par['normalize'])

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and resize the textfields according to content.
        """
        self.delays = safe_eval(self.led_delays.text(),
                                self.delays,
                                return_type='int',
                                sign='pos')
        self.led_delays.setText(str(self.delays))
        self.stages = safe_eval(self.led_stages.text(),
                                self.stages,
                                return_type='int',
                                sign='pos')
        self.led_stages.setText(str(self.stages))

        self._store_entries()

    def _store_entries(self):
        """
        Store parameter settings in filter dictionary. Called from _update_UI()
        and _save()
        """
        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update({
            'ma': {
                'delays': self.delays,
                'stages': self.stages,
                'normalize': self.chk_norm.isChecked()
            }
        })
        # sig_tx -> select_filter -> filter_specs
        self.emit({'filt_changed': 'ma'})

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        # N is total order, L is number of taps per stage
        self.F_SB = fil_dict['F_SB']
        self.A_SB = fil_dict['A_SB']

    def _save(self, fil_dict):
        """
        Save MA-filters both in 'zpk' and 'ba' format; no conversion has to be
        performed except maybe deleting an 'sos' entry from an earlier
        filter design.
        """
        if 'zpk' in self.FRMT:
            fil_save(fil_dict, self.zpk, 'zpk', __name__, convert=False)

        if 'ba' in self.FRMT:
            fil_save(fil_dict, self.b, 'ba', __name__, convert=False)

        fil_convert(fil_dict, self.FRMT)

        # always update filter dict and LineEdit, in case the design algorithm
        # has changed the number of delays:
        fil_dict['N'] = self.delays * self.stages  # updated filter order
        self.led_delays.setText(str(self.delays))  # updated number of delays

        self._store_entries()

    def calc_ma(self, fil_dict, rt='LP'):
        """
        Calculate coefficients and P/Z for moving average filter based on
        filter length L = N + 1 and number of cascaded stages and save the
        result in the filter dictionary.
        """
        b = 1.
        k = 1.
        L = self.delays + 1

        if rt == 'LP':
            b0 = np.ones(L)  #  h[n] = {1; 1; 1; ...}
            i = np.arange(1, L)

            norm = L

        elif rt == 'HP':
            b0 = np.ones(L)
            b0[::2] = -1.  # h[n] = {1; -1; 1; -1; ...}

            i = np.arange(L)
            if (L % 2 == 0):  # even order, remove middle element
                i = np.delete(i, round(L / 2.))
            else:  # odd order, shift by 0.5 and remove middle element
                i = np.delete(i, int(L / 2.)) + 0.5

            norm = L

        elif rt == 'BP':
            # N is even, L is odd
            b0 = np.ones(L)
            b0[1::2] = 0
            b0[::4] = -1  # h[n] = {1; 0; -1; 0; 1; ... }

            L = L + 1
            i = np.arange(L)  # create N + 2 zeros around the unit circle, ...
            # ... remove first and middle element and rotate by L / 4
            i = np.delete(i, [0, L // 2]) + L / 4

            norm = np.sum(abs(b0))

        elif rt == 'BS':
            # N is even, L is odd
            b0 = np.ones(L)
            b0[1::2] = 0

            L = L + 1
            i = np.arange(
                L)  # create N + 2 zeros around the unit circle and ...
            i = np.delete(i,
                          [0, L // 2])  # ... remove first and middle element

            norm = np.sum(b0)

        if self.delays > 1000:
            if not qfilter_warning(None, self.delays * self.stages,
                                   "Moving Average"):
                return -1

        z0 = np.exp(-2j * np.pi * i / L)
        # calculate filter for multiple cascaded stages
        for i in range(self.stages):
            b = np.convolve(b0, b)
        z = np.repeat(z0, self.stages)

        # normalize filter to |H_max| = 1 if checked:
        if self.chk_norm.isChecked():
            b = b / (norm**self.stages)
            k = 1. / norm**self.stages
        p = np.zeros(len(z))

        # store in class attributes for the _save method
        self.zpk = [z, p, k]
        self.b = b
        self._save(fil_dict)

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='LP')

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = int(
            np.ceil(
                1 /
                (self.A_SB**(1 / self.stages) * np.sin(self.F_SB * np.pi))))
        self.calc_ma(fil_dict, rt='LP')

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='HP')

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = int(
            np.ceil(1 / (self.A_SB**(1 / self.stages) * np.sin(
                (0.5 - self.F_SB) * np.pi))))
        self.calc_ma(fil_dict, rt='HP')

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = ceil_odd(self.delays)  # enforce odd order
        self.calc_ma(fil_dict, rt='BS')

    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = ceil_odd(self.delays)  # enforce odd order
        self.calc_ma(fil_dict, rt='BP')
Exemplo n.º 14
0
class Equiripple(QWidget):

    FRMT = 'ba'  # output format of filter design routines 'zpk' / 'ba' / 'sos'
    # currently, only 'ba' is supported for equiripple routines

    info = """
**Equiripple filters**

have the steepest rate of transition between the frequency response’s passband
and stopband of all FIR filters. This comes at the expense of a constant ripple
(equiripple) :math:`A_PB` and :math:`A_SB` in both pass and stop band.

The filter-coefficients are calculated in such a way that the transfer function
minimizes the maximum error (**Minimax** design) between the desired gain and the
realized gain in the specified frequency bands using the **Remez** exchange algorithm.
The filter design algorithm is known as **Parks-McClellan** algorithm, in
Matlab (R) it is called ``firpm``.

Manual filter order design requires specifying the frequency bands (:math:`F_PB`,
:math:`f_SB` etc.), the filter order :math:`N` and weight factors :math:`W_PB`,
:math:`W_SB` etc.) for individual bands.

The minimum order and the weight factors needed to fulfill the target specifications
is estimated from frequency and amplitude specifications using Ichige's algorithm.

**Design routines:**

``scipy.signal.remez()``, ``pyfda_lib.remezord()``
    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.grid_density = 16

        self.ft = 'FIR'

        self.rt_dicts = ('com', )

        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('a', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter desired filter order <b><i>N</i></b>, corner "
                     "frequencies of pass and stop band(s), <b><i>F<sub>PB</sub></i></b>"
                     "&nbsp; and <b><i>F<sub>SB</sub></i></b>&nbsp;, and relative weight "
                     "values <b><i>W&nbsp; </i></b> (1 ... 10<sup>6</sup>) to specify how well "
                     "the bands are approximated.</span>")
                },
                'min': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter the maximum pass band ripple <b><i>A<sub>PB</sub></i></b>, "
                     "minimum stop band attenuation <b><i>A<sub>SB</sub></i></b> "
                     "and the corresponding corner frequencies of pass and "
                     "stop band(s), <b><i>F<sub>PB</sub></i></b>&nbsp; and "
                     "<b><i>F<sub>SB</sub></i></b> .</span>")
                }
            },
            'LP': {
                'man': {
                    'wspecs': ('a', 'W_PB', 'W_SB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('u', 'A_PB', 'A_SB')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_PB', 'W_SB'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('a', 'A_PB', 'A_SB')
                    })
                }
            },
            'HP': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('u', 'A_SB', 'A_PB')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_SB', 'W_PB'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('a', 'A_SB', 'A_PB')
                    })
                }
            },
            'BP': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('a', 'A_SB', 'A_PB', 'A_SB2')
                    })
                },
            },
            'BS': {
                'man': {
                    'wspecs': ('a', 'W_PB', 'W_SB', 'W_PB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('u', 'A_PB', 'A_SB', 'A_PB2')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_PB', 'W_SB', 'W_PB2'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('a', 'A_PB', 'A_SB', 'A_PB2')
                    })
                }
            },
            'HIL': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    })
                }
            },
            'DIFF': {
                'man': {
                    'wspecs': ('a', 'W_PB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB'),
                        'amp': ('i', )
                    }),
                    'msg':
                    ('a',
                     "Enter the max. frequency up to where the differentiator "
                     "works.")
                }
            }
        }

        self.info_doc = []
        self.info_doc.append('remez()\n=======')
        self.info_doc.append(sig.remez.__doc__)
        self.info_doc.append('remezord()\n==========')
        self.info_doc.append(remezord.__doc__)

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in 
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        self.lbl_remez_1 = QLabel("Grid Density", self)
        self.lbl_remez_1.setObjectName('wdg_lbl_remez_1')
        self.led_remez_1 = QLineEdit(self)
        self.led_remez_1.setText(str(self.grid_density))
        self.led_remez_1.setObjectName('wdg_led_remez_1')
        self.led_remez_1.setToolTip(
            "Number of frequency points for Remez algorithm. Increase the\n"
            "number to reduce frequency overshoot in the transition region.")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_remez_1)
        self.layHWin.addWidget(self.led_remez_1)
        self.layHWin.setContentsMargins(0, 0, 0, 0)
        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.led_remez_1.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.grid_density = safe_eval(self.led_remez_1.text(),
                                      self.grid_density,
                                      return_type='int',
                                      sign='pos')
        self.led_remez_1.setText(str(self.grid_density))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'equiripple': {
                'grid_density': self.grid_density
            }})

        # sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'equiripple'})

    def _load_dict(self):
        """
        Reload parameter(s) from filter dictionary (if they exist) and set 
        corresponding UI elements. _load_dict() is called upon initialization
        and when the filter is loaded from disk.
        """
        if 'wdg_fil' in fb.fil[0] and 'equiripple' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['equiripple']
            if 'grid_density' in wdg_fil_par:
                self.grid_density = wdg_fil_par['grid_density']
                self.led_remez_1.setText(str(self.grid_density))

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N = fil_dict['N'] + 1  # remez algorithms expects number of taps
        # which is larger by one than the order!!
        self.F_PB = fil_dict['F_PB']
        self.F_SB = fil_dict['F_SB']
        self.F_PB2 = fil_dict['F_PB2']
        self.F_SB2 = fil_dict['F_SB2']
        # remez amplitude specs are linear (not in dBs)
        self.A_PB = fil_dict['A_PB']
        self.A_PB2 = fil_dict['A_PB2']
        self.A_SB = fil_dict['A_SB']
        self.A_SB2 = fil_dict['A_SB2']

        self.alg = 'ichige'

    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 2000:
            return qfilter_warning(self, self.N, "Equiripple")
        else:
            return True

    def _save(self, fil_dict, arg):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        try:
            fil_save(fil_dict, arg, self.FRMT, __name__)
        except Exception as e:
            # catch exception due to malformatted coefficients:
            logger.error("While saving the equiripple filter design, "
                         "the following error occurred:\n{0}".format(e))
            return -1

        if str(fil_dict['fo']) == 'min':
            fil_dict['N'] = self.N - 1  # yes, update filterbroker

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB, self.F_SB, 0.5], [1, 0],
                      weight=[fil_dict['W_PB'], fil_dict['W_SB']],
                      fs=1,
                      grid_density=self.grid_density))

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_PB, self.F_SB], [1, 0],
                                     [self.A_PB, self.A_SB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        if (self.N % 2 == 0):  # even order, use odd symmetry (type III)
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:  # odd order,
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_SB, self.F_PB], [0, 1],
                                     [self.A_SB, self.A_PB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
#        self.N = ceil_odd(N)  # enforce odd order
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        if (self.N % 2 == 0):  # even order
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    # For BP and BS, F_PB and F_SB have two elements each
    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                grid_density=self.grid_density))

    def BPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A,
         W) = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2],
                       [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2],
                       fs=1,
                       alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        fil_dict['W_SB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [0, 1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = round_odd(self.N)  # enforce odd order
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_PB, self.F_SB, self.F_SB2, self.F_PB2, 0.5],
                [1, 0, 1],
                weight=[fil_dict['W_PB'], fil_dict['W_SB'], fil_dict['W_PB2']],
                fs=1,
                grid_density=self.grid_density))

    def BSmin(self, fil_dict):
        self._get_params(fil_dict)
        (N, F, A, W) = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2],
                                [1, 0, 1], [self.A_PB, self.A_SB, self.A_PB2],
                                fs=1,
                                alg=self.alg)
        self.N = round_odd(N)  # enforce odd order
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        fil_dict['W_PB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0, 1],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HILman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                type='hilbert',
                grid_density=self.grid_density))

    def DIFFman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = ceil_even(self.N)  # enforce even order
        if self.F_PB < 0.1:
            logger.warning(
                "Bandwidth for pass band ({0}) is too low, inreasing to 0.1".
                format(self.F_PB))
            self.F_PB = 0.1
            fil_dict['F_PB'] = self.F_PB
            self.sig_tx.emit({
                'sender': __name__,
                'specs_changed': 'equiripple'
            })

        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB], [np.pi * fil_dict['W_PB']],
                      fs=1,
                      type='differentiator',
                      grid_density=self.grid_density))
Exemplo n.º 15
0
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()
Exemplo n.º 16
0
class Plot_PZ(QWidget):
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self):
        super().__init__()
        self.needs_calc = True   # flag whether filter data has been changed
        self.needs_draw = False  # flag whether whether figure needs to be drawn
                                 # with new limits etc. (not implemented yet)
        self.tool_tip = "Pole / zero plan"
        self.tab_label = "P / Z"

        self.cmb_overlay_items = [
            "<span>Add various overlays to P/Z diagram.</span>",
            ("none", "None", ""),
            ("h(f)", "|H(f)|",
             "<span>Show |H(f)| wrapped around the unit circle between 0 resp. -120 dB "
             "and max(H(f)).</span>"),
            ("contour", "Contour", "<span>Show contour lines for |H(z)|</span>"),
            ("contourf", "Contourf", "<span>Show filled contours for |H(z)|</span>"),
            ]
        self.cmb_overlay_default = "none"  # default setting
        self.cmap = "viridis"  # colormap

        self.zmin = 0
        self.zmax = 2
        self.zmin_dB = -80
        self.zmax_dB = np.round(20 * np.log10(self.zmax), 2)
        self._construct_UI()

# ------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig: dict = None) -> None:
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        # logger.info("Processing {0} | needs_draw = {1}, visible = {2}"\
        #              .format(dict_sig, self.needs_calc, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc:
                self.draw()
                self.needs_calc = False
                self.needs_draw = False
            elif 'view_changed' in dict_sig or self.needs_draw:
                self.update_view()
                self.needs_draw = False
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.draw()
        else:
            if 'data_changed' in dict_sig:
                self.needs_calc = True
            elif 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.needs_draw = True

# ------------------------------------------------------------------------------
    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.lbl_overlay = QLabel(to_html("Overlay:", frmt='bi'), self)
        self.cmb_overlay = QComboBox(self)
        qcmb_box_populate(
            self.cmb_overlay, self.cmb_overlay_items, self.cmb_overlay_default)

        self.but_log = PushButton(" Log.", checked=True)
        self.but_log.setObjectName("but_log")
        self.but_log.setToolTip("<span>Log. scale for overlays.</span>")

        self.diaRad_Hf = QDial(self)
        self.diaRad_Hf.setRange(2, 10)
        self.diaRad_Hf.setValue(2)
        self.diaRad_Hf.setTracking(False)  # produce less events when turning
        self.diaRad_Hf.setFixedHeight(30)
        self.diaRad_Hf.setFixedWidth(30)
        self.diaRad_Hf.setWrapping(False)
        self.diaRad_Hf.setToolTip("<span>Set max. radius for |H(f)| plot.</span>")

        self.lblRad_Hf = QLabel("Radius", self)

        self.lblBottom = QLabel(to_html("Bottom =", frmt='bi'), self)
        self.ledBottom = QLineEdit(self)
        self.ledBottom.setObjectName("ledBottom")
        self.ledBottom.setText(str(self.zmin))
        self.ledBottom.setMaximumWidth(qtext_width(N_x=8))
        self.ledBottom.setToolTip("Minimum display value.")
        self.lblBottomdB = QLabel("dB", self)
        self.lblBottomdB.setVisible(self.but_log.isChecked())

        self.lblTop = QLabel(to_html("Top =", frmt='bi'), self)
        self.ledTop = QLineEdit(self)
        self.ledTop.setObjectName("ledTop")
        self.ledTop.setText(str(self.zmax))
        self.ledTop.setToolTip("Maximum display value.")
        self.ledTop.setMaximumWidth(qtext_width(N_x=8))
        self.lblTopdB = QLabel("dB", self)
        self.lblTopdB.setVisible(self.but_log.isChecked())

        self.but_fir_poles = PushButton(" FIR Poles ", checked=True)
        self.but_fir_poles.setToolTip("<span>Show FIR poles at the origin.</span>")

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.lbl_overlay)
        layHControls.addWidget(self.cmb_overlay)
        layHControls.addWidget(self.but_log)
        layHControls.addWidget(self.diaRad_Hf)
        layHControls.addWidget(self.lblRad_Hf)
        layHControls.addWidget(self.lblTop)
        layHControls.addWidget(self.ledTop)
        layHControls.addWidget(self.lblTopdB)
        layHControls.addWidget(self.lblBottom)
        layHControls.addWidget(self.ledBottom)
        layHControls.addWidget(self.lblBottomdB)
        layHControls.addStretch(10)
        layHControls.addWidget(self.but_fir_poles)

        # ----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        # ----------------------------------------------------------------------
        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layHControls)

        # ----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets
        # ----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.mplwidget.mplToolbar.a_he.setEnabled(True)
        self.mplwidget.mplToolbar.a_he.info = "manual/plot_pz.html"
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self._log_clicked()  # calculate and draw poles and zeros

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.cmb_overlay.currentIndexChanged.connect(self.draw)
        self.but_log.clicked.connect(self._log_clicked)
        self.ledBottom.editingFinished.connect(self._log_clicked)
        self.ledTop.editingFinished.connect(self._log_clicked)
        self.diaRad_Hf.valueChanged.connect(self.draw)
        self.but_fir_poles.clicked.connect(self.draw)

    # --------------------------------------------------------------------------
    def _log_clicked(self):
        """
        Change scale and settings to log / lin when log setting is changed
        Update min / max settings when lineEdits have been edited
        """
        # clicking but_log triggered the slot or initialization
        if self.sender() is None or self.sender().objectName() == 'but_log':
            if self.but_log.isChecked():
                self.ledBottom.setText(str(self.zmin_dB))
                self.zmax_dB = np.round(20 * np.log10(self.zmax), 2)
                self.ledTop.setText(str(self.zmax_dB))
            else:
                self.ledBottom.setText(str(self.zmin))
                self.zmax = np.round(10**(self.zmax_dB / 20), 2)
                self.ledTop.setText(str(self.zmax))

        else:  # finishing a lineEdit field triggered the slot
            if self.but_log.isChecked():
                self.zmin_dB = safe_eval(
                    self.ledBottom.text(), self.zmin_dB, return_type='float')
                self.ledBottom.setText(str(self.zmin_dB))
                self.zmax_dB = safe_eval(
                    self.ledTop.text(), self.zmax_dB, return_type='float')
                self.ledTop.setText(str(self.zmax_dB))
            else:
                self.zmin = safe_eval(
                    self.ledBottom.text(), self.zmin, return_type='float')
                self.ledBottom.setText(str(self.zmin))
                self.zmax = safe_eval(self.ledTop.text(), self.zmax, return_type='float')
                self.ledTop.setText(str(self.zmax))

        self.draw()

    # --------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes (this is only run once)
        """
        self.mplwidget.fig.clf()  # needed to get rid of colorbar
        if len(self.mplwidget.fig.get_axes()) == 0:  # empty figure, no axes
            self.ax = self.mplwidget.fig.subplots()  # .add_subplot(111)
        self.ax.xaxis.tick_bottom()  # remove axis ticks on top
        self.ax.yaxis.tick_left()  # remove axis ticks right

    # --------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etcs without recalculating H(f)
        -- not yet implemented, just use draw() for the moment
        """
        self.draw()

    # --------------------------------------------------------------------------
    def draw(self):
        self.but_fir_poles.setVisible(fb.fil[0]['ft'] == 'FIR')
        contour = qget_cmb_box(self.cmb_overlay) in {"contour", "contourf"}
        self.ledBottom.setVisible(contour)
        self.lblBottom.setVisible(contour)
        self.lblBottomdB.setVisible(contour and self.but_log.isChecked())
        self.ledTop.setVisible(contour)
        self.lblTop.setVisible(contour)
        self.lblTopdB.setVisible(contour and self.but_log.isChecked())

        if True:
            self.init_axes()
        self.draw_pz()

    # --------------------------------------------------------------------------
    def draw_pz(self):
        """
        (re)draw P/Z plot
        """
        p_marker = params['P_Marker']
        z_marker = params['Z_Marker']

        zpk = fb.fil[0]['zpk']

        # add antiCausals if they exist (must take reciprocal to plot)
        if 'rpk' in fb.fil[0]:
            zA = fb.fil[0]['zpk'][0]
            zA = np.conj(1./zA)
            pA = fb.fil[0]['zpk'][1]
            pA = np.conj(1./pA)
            zC = np.append(zpk[0], zA)
            pC = np.append(zpk[1], pA)
            zpk[0] = zC
            zpk[1] = pC

        self.ax.clear()

        [z, p, k] = self.zplane(
            z=zpk[0], p=zpk[1], k=zpk[2], plt_ax=self.ax,
            plt_poles=self.but_fir_poles.isChecked() or fb.fil[0]['ft'] == 'IIR',
            mps=p_marker[0], mpc=p_marker[1], mzs=z_marker[0], mzc=z_marker[1])

        self.ax.xaxis.set_minor_locator(AutoMinorLocator())  # enable minor ticks
        self.ax.yaxis.set_minor_locator(AutoMinorLocator())  # enable minor ticks
        self.ax.set_title(r'Pole / Zero Plot')
        self.ax.set_xlabel('Real axis')
        self.ax.set_ylabel('Imaginary axis')

        overlay = qget_cmb_box(self.cmb_overlay)
        self.but_log.setVisible(overlay != "none")

        self.draw_Hf(r=self.diaRad_Hf.value(), Hf_visible=(overlay == "h(f)"))

        self.draw_contours(overlay)

        self.redraw()

    # --------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()

    # --------------------------------------------------------------------------
    def zplane(self, b=None, a=1, z=None, p=None, k=1,  pn_eps=1e-3, analog=False,
               plt_ax=None, plt_poles=True, style='equal', anaCircleRad=0, lw=2,
               mps=10, mzs=10, mpc='r', mzc='b', plabel='', zlabel=''):
        """
        Plot the poles and zeros in the complex z-plane either from the
        coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False)
        or directly from the zeros and poles (z,p) (zpk = True).

        When only b is given, an FIR filter with all poles at the origin is assumed.

        Parameters
        ----------
        b :  array_like
             Numerator coefficients (transversal part of filter)
             When b is not None, poles and zeros are determined from the coefficients
             b and a

        a :  array_like (optional, default = 1 for FIR-filter)
             Denominator coefficients (recursive part of filter)

        z :  array_like, default = None
             Zeros
             When b is None, poles and zeros are taken directly from z and p

        p :  array_like, default = None
             Poles

        analog : boolean (default: False)
            When True, create a P/Z plot suitable for the s-plane, i.e. suppress
            the unit circle (unless anaCircleRad > 0) and scale the plot for
            a good display of all poles and zeros.

        pn_eps : float (default : 1e-2)
             Tolerance for separating close poles or zeros

        plt_ax : handle to axes for plotting (default: None)
            When no axes is specified, the current axes is determined via plt.gca()

        plt_poles : Boolean (default : True)
            Plot poles. This can be used to suppress poles for FIR systems
            where all poles are at the origin.

        style : string (default: 'scaled')
            Style of the plot, for `style == 'scaled'` make scale of x- and y-
            axis equal, `style == 'equal'` forces x- and y-axes to be equal. This
            is passed as an argument to the matplotlib `ax.axis(style)`

        mps : integer  (default: 10)
            Size for pole marker

        mzs : integer (default: 10)
            Size for zero marker

        mpc : char (default: 'r')
            Pole marker colour

        mzc : char (default: 'b')
            Zero marker colour

        lw : integer (default:  2)
            Linewidth for unit circle

        plabel, zlabel : string (default: '')
            This string is passed to the plot command for poles and zeros and
            can be displayed by legend()


        Returns
        -------
        z, p, k : ndarray


        Notes
        -----
        """
        # TODO:
        # - polar option
        # - add keywords for color of circle -> **kwargs
        # - add option for multi-dimensional arrays and zpk data

        # make sure that all inputs are (at least 1D) arrays
        b = np.atleast_1d(b)
        a = np.atleast_1d(a)
        z = np.atleast_1d(z)
        p = np.atleast_1d(p)

        if b.any():  # coefficients were specified
            if len(b) < 2 and len(a) < 2:
                logger.error('No proper filter coefficients: both b and a are scalars!')
                return z, p, k

            # The coefficients are less than 1, normalize the coefficients
            if np.max(b) > 1:
                kn = np.max(b)
                b = b / float(kn)
            else:
                kn = 1.

            if np.max(a) > 1:
                kd = np.max(a)
                a = a / abs(kd)
            else:
                kd = 1.

            # Calculate the poles, zeros and scaling factor
            p = np.roots(a)
            z = np.roots(b)
            k = kn/kd
        elif not (len(p) or len(z)):  # P/Z were specified
            logger.error('Either b,a or z,p must be specified!')
            return z, p, k

        # find multiple poles and zeros and their multiplicities
        if len(p) < 2:  # single pole, [None] or [0]
            if not p or p == 0:  # only zeros, create equal number of poles at origin
                p = np.array(0, ndmin=1)
                num_p = np.atleast_1d(len(z))
            else:
                num_p = [1.]  # single pole != 0
        else:
            # p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg')
            p, num_p = unique_roots(p, tol=pn_eps, rtype='avg')
    #        p = np.array(p); num_p = np.ones(len(p))
        if len(z) > 0:
            z, num_z = unique_roots(z, tol=pn_eps, rtype='avg')
    #        z = np.array(z); num_z = np.ones(len(z))
            # z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg')
        else:
            num_z = []

        if analog is False:
            # create the unit circle for the z-plane
            uc = patches.Circle((0, 0), radius=1, fill=False,
                                color='grey', ls='solid', zorder=1)
            plt_ax.add_patch(uc)
            plt_ax.axis(style)
        #    ax.spines['left'].set_position('center')
        #    ax.spines['bottom'].set_position('center')
        #    ax.spines['right'].set_visible(True)
        #    ax.spines['top'].set_visible(True)

        else:  # s-plane
            if anaCircleRad > 0:
                # plot a circle with radius = anaCircleRad
                uc = patches.Circle((0, 0), radius=anaCircleRad, fill=False,
                                    color='grey', ls='solid', zorder=1)
                plt_ax.add_patch(uc)
            # plot real and imaginary axis
            plt_ax.axhline(lw=2, color='k', zorder=1)
            plt_ax.axvline(lw=2, color='k', zorder=1)

        # Plot the zeros
        plt_ax.scatter(z.real, z.imag, s=mzs*mzs, zorder=2, marker='o',
                       facecolor='none', edgecolor=mzc, lw=lw, label=zlabel)
        # and print their multiplicity
        for i in range(len(z)):
            logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i]))
            if num_z[i] > 1:
                plt_ax.text(np.real(z[i]), np.imag(z[i]), '  (' + str(num_z[i]) + ')',
                            va='top', color=mzc)
        if plt_poles:
            # Plot the poles
            plt_ax.scatter(p.real, p.imag, s=mps*mps, zorder=2, marker='x',
                           color=mpc, lw=lw, label=plabel)
            # and print their multiplicity
            for i in range(len(p)):
                logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i]))
                if num_p[i] > 1:
                    plt_ax.text(np.real(p[i]), np.imag(p[i]), '  (' + str(num_p[i]) + ')',
                                va='bottom', color=mpc)

# =============================================================================
#            # increase distance between ticks and labels
#            # to give some room for poles and zeros
#         for tick in ax.get_xaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#         for tick in ax.get_yaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#
# =============================================================================
        xl = plt_ax.get_xlim()
        Dx = max(abs(xl[1]-xl[0]), 0.05)
        yl = plt_ax.get_ylim()
        Dy = max(abs(yl[1]-yl[0]), 0.05)

        plt_ax.set_xlim((xl[0]-Dx*0.02, max(xl[1]+Dx*0.02, 0)))
        plt_ax.set_ylim((yl[0]-Dy*0.02, yl[1] + Dy*0.02))

        return z, p, k

    # --------------------------------------------------------------------------
    def draw_contours(self, overlay):
        if overlay not in {"contour", "contourf"}:
            return
        self.ax.apply_aspect()  # normally, the correct aspect is only set when plotting
        xl = self.ax.get_xlim()
        yl = self.ax.get_ylim()
        # logger.warning(xl)
        # logger.warning(yl)

        [x, y] = np.meshgrid(
            np.arange(xl[0], xl[1], 0.01),
            np.arange(yl[0], yl[1], 0.01))
        z = x + 1j*y  # create coordinate grid for complex plane

        if self.but_log.isChecked():
            H_max = self.zmax_dB
            H_min = self.zmin_dB
        else:
            H_max = self.zmax
            H_min = self.zmin
        Hmag = H_mag(fb.fil[0]['ba'][0], fb.fil[0]['ba'][1], z, H_max, H_min=H_min,
                     log=self.but_log.isChecked())

        if overlay == "contour":
            self.ax.contour(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap)
        else:
            self.ax.contourf(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap)

        m_cb = cm.ScalarMappable(cmap=self.cmap)    # normalized proxy object that is
        m_cb.set_array(Hmag)                        # mappable for colorbar (?)
        self.col_bar = self.mplwidget.fig.colorbar(
            m_cb, ax=self.ax, shrink=1.0, aspect=40, pad=0.01, fraction=0.08)

        # Contour plots and color bar somehow mess up the coordinates:
        # restore to previous settings
        self.ax.set_xlim(xl)
        self.ax.set_xlim(yl)

    # --------------------------------------------------------------------------
    def draw_Hf(self, r=2, Hf_visible=True):
        """
        Draw the magnitude frequency response around the UC
        """
        self.diaRad_Hf.setVisible(Hf_visible)
        self.lblRad_Hf.setVisible(Hf_visible)
        if not Hf_visible:
            return

        # suppress "divide by zero in log10" warnings
        old_settings_seterr = np.seterr()
        np.seterr(divide='ignore')
        ba = fb.fil[0]['ba']
        w, H = sig.freqz(ba[0], ba[1], worN=params['N_FFT'], whole=True)
        H = np.abs(H)
        if self.but_log.isChecked():
            H = np.clip(np.log10(H), -6, None)  # clip to -120 dB
            H = H - np.max(H)  # shift scale to H_min ... 0
            H = 1 + (r-1) * (1 + H / abs(np.min(H)))  # scale to 1 ... r
        else:
            H = 1 + (r-1) * H / np.max(H)  # map |H(f)| to a range 1 ... r
        y = H * np.sin(w)
        x = H * np.cos(w)

        self.ax.plot(x, y, label="|H(f)|")
        uc = patches.Circle((0, 0), radius=r, fill=False,
                            color='grey', ls='dashed', zorder=1)
        self.ax.add_patch(uc)

        xl = self.ax.get_xlim()
        xmax = max(abs(xl[0]), abs(xl[1]), r*1.05)
        yl = self.ax.get_ylim()
        ymax = max(abs(yl[0]), abs(yl[1]), r*1.05)
        self.ax.set_xlim((-xmax, xmax))
        self.ax.set_ylim((-ymax, ymax))

        np.seterr(**old_settings_seterr)