Esempio n. 1
0
class FreqUnits(QWidget):
    """
    Build and update widget for entering frequency unit, frequency range and
    sampling frequency f_S

    The following key-value pairs of the `fb.fil[0]` dict are modified:

        - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string
        - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency
                               values for labelling the frequency axis
        - `'f_S'` : The sampling frequency for referring frequency values to as a float
        - `'f_max'` : maximum frequency for scaling frequency axis
        - `'plt_fUnit'`: frequency unit as string
        - `'plt_tUnit'`: time unit as string
        - `'plt_fLabel'`: label for frequency axis
        - `'plt_tLabel'`: label for time axis

    """

    # class variables (shared between instances if more than one exists)
    sig_tx = pyqtSignal(object)  # outgoing
    from pyfda.libs.pyfda_qt_lib import emit

    def __init__(self, parent=None, title="Frequency Units"):

        super(FreqUnits, self).__init__(parent)
        self.title = title
        self.spec_edited = False  # flag whether QLineEdit field has been edited

        self._construct_UI()

    def _construct_UI(self):
        """
        Construct the User Interface
        """
        self.layVMain = QVBoxLayout()  # Widget main layout

        f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz']
        self.t_units = ['', 'T_S', 'T_S', 's', 'ms', r'$\mu$s', 'ns']

        bfont = QFont()
        bfont.setBold(True)

        self.lblUnits = QLabel(self)
        self.lblUnits.setText("Freq. Unit")
        self.lblUnits.setFont(bfont)

        self.fs_old = fb.fil[0]['f_S']  # store current sampling frequency

        self.lblF_S = QLabel(self)
        self.lblF_S.setText(to_html("f_S =", frmt='bi'))

        self.ledF_S = QLineEdit()
        self.ledF_S.setText(str(fb.fil[0]["f_S"]))
        self.ledF_S.setObjectName("f_S")
        self.ledF_S.installEventFilter(self)  # filter events

        self.butLock = QToolButton(self)
        self.butLock.setIcon(QIcon(':/lock-unlocked.svg'))
        self.butLock.setCheckable(True)
        self.butLock.setChecked(False)
        self.butLock.setToolTip(
            "<span><b>Unlocked:</b> When f_S is changed, all frequency related "
            "widgets are updated, normalized frequencies stay the same.<br />"
            "<b>Locked:</b> When f_S is changed, displayed absolute frequency "
            "values don't change but normalized frequencies do.</span>")
        # self.butLock.setStyleSheet("QToolButton:checked {font-weight:bold}")

        layHF_S = QHBoxLayout()
        layHF_S.addWidget(self.ledF_S)
        layHF_S.addWidget(self.butLock)

        self.cmbUnits = QComboBox(self)
        self.cmbUnits.setObjectName("cmbUnits")
        self.cmbUnits.addItems(f_units)
        self.cmbUnits.setToolTip(
            'Select whether frequencies are specified w.r.t. \n'
            'the sampling frequency "f_S", to the Nyquist frequency \n'
            'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S '
            'but plots graphs over the frequency index k.')
        self.cmbUnits.setCurrentIndex(1)
        #        self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))#
        #        self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole)

        fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")]
        self.cmbFRange = QComboBox(self)
        self.cmbFRange.setObjectName("cmbFRange")
        for f in fRanges:
            self.cmbFRange.addItem(f[0], f[1])
        self.cmbFRange.setToolTip("Select frequency range (whole or half).")
        self.cmbFRange.setCurrentIndex(0)

        # Combobox resizes with longest entry
        self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.butSort = QToolButton(self)
        self.butSort.setText("Sort")
        self.butSort.setIcon(QIcon(':/sort-ascending.svg'))
        #self.butDelCells.setIconSize(q_icon_size)
        self.butSort.setCheckable(True)
        self.butSort.setChecked(True)
        self.butSort.setToolTip(
            "Sort frequencies in ascending order when pushed.")
        self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}")

        self.layHUnits = QHBoxLayout()
        self.layHUnits.addWidget(self.cmbUnits)
        self.layHUnits.addWidget(self.cmbFRange)
        self.layHUnits.addWidget(self.butSort)

        # Create a gridLayout consisting of QLabel and QLineEdit fields
        # for setting f_S, the units and the actual frequency specs:
        self.layGSpecWdg = QGridLayout()  # sublayout for spec fields
        self.layGSpecWdg.addWidget(self.lblF_S, 1, 0)
        # self.layGSpecWdg.addWidget(self.ledF_S,1,1)
        self.layGSpecWdg.addLayout(layHF_S, 1, 1)
        self.layGSpecWdg.addWidget(self.lblUnits, 0, 0)
        self.layGSpecWdg.addLayout(self.layHUnits, 0, 1)

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

        self.layVMain.addWidget(frmMain)
        self.layVMain.setContentsMargins(*params['wdg_margins'])

        self.setLayout(self.layVMain)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.cmbUnits.currentIndexChanged.connect(self.update_UI)
        self.butLock.clicked.connect(self._lock_freqs)
        self.cmbFRange.currentIndexChanged.connect(self._freq_range)
        self.butSort.clicked.connect(self._store_sort_flag)
        # ----------------------------------------------------------------------

        self.update_UI()  # first-time initialization

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

    def _lock_freqs(self):
        """
        Lock / unlock frequency entries: The values of frequency related widgets
        are stored in normalized form (w.r.t. sampling frequency)`fb.fil[0]['f_S']`.

        When the sampling frequency changes, absolute frequencies displayed in the
        widgets change their values. Most of the time, this is the desired behaviour,
        the properties of discrete time systems or signals are usually defined
        by the normalized frequencies.

        When the effect of varying the sampling frequency is to be analyzed, the
        displayed values in the widgets can be locked by pressing the Lock button.
        After changing the sampling frequency, normalized frequencies have to be
        rescaled like `f_a *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']` to maintain
        the displayed value `f_a * f_S`.

        This has to be accomplished by each frequency widget (currently, these are
        freq_specs and freq_units).

        The setting is stored as bool in the global dict entry `fb.fil[0]['freq_locked'`,
        the signal 'view_changed':'f_S' is emitted.
        """

        if self.butLock.isChecked():
            # Lock has been activated, keep displayed frequencies locked
            fb.fil[0]['freq_locked'] = True
            self.butLock.setIcon(QIcon(':/lock-locked.svg'))
        else:
            # Lock has been unlocked, scale displayed frequencies with f_S
            fb.fil[0]['freq_locked'] = False
            self.butLock.setIcon(QIcon(':/lock-unlocked.svg'))

        self.emit({'view_changed': 'f_S'})

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

    def update_UI(self):
        """
        update_UI is called
        - during init
        - when the unit combobox is changed

        Set various scale factors and labels depending on the setting of the unit
        combobox.

        Update the freqSpecsRange and finally, emit 'view_changed':'f_S' signal
        """
        f_unit = str(self.cmbUnits.currentText())  # selected frequency unit
        idx = self.cmbUnits.currentIndex()  # and its index

        is_normalized_freq = f_unit in {"f_S", "f_Ny", "k"}

        self.ledF_S.setVisible(not is_normalized_freq)  # only vis. when
        self.lblF_S.setVisible(not is_normalized_freq)  # not normalized
        self.butLock.setVisible(not is_normalized_freq)
        f_S_scale = 1  # default setting for f_S scale

        if is_normalized_freq:
            # store current sampling frequency to restore it when returning to
            # unnormalized frequencies
            self.fs_old = fb.fil[0]['f_S']

            if f_unit == "f_S":  # normalized to f_S
                fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1.
                fb.fil[0]['T_S'] = 1.
                f_label = r"$F = f\, /\, f_S = \Omega \, /\,  2 \mathrm{\pi} \; \rightarrow$"
                t_label = r"$n = t\, /\, T_S \; \rightarrow$"
            elif f_unit == "f_Ny":  # normalized to f_nyq = f_S / 2
                fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2.
                fb.fil[0]['T_S'] = 1.
                f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$"
                t_label = r"$n = t\, /\, T_S \; \rightarrow$"
            else:  # frequency index k,
                fb.fil[0]['f_S'] = 1.
                fb.fil[0]['T_S'] = 1.
                fb.fil[0]['f_max'] = params['N_FFT']
                f_label = r"$k \; \rightarrow$"
                t_label = r"$n\; \rightarrow$"

            self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

        else:  # Hz, kHz, ...
            # Restore sampling frequency when returning from f_S / f_Ny / k
            if fb.fil[0]['freq_specs_unit'] in {
                    "f_S", "f_Ny", "k"
            }:  # previous setting normalized?
                fb.fil[0]['f_S'] = fb.fil[0][
                    'f_max'] = self.fs_old  # yes, restore prev.
                fb.fil[0][
                    'T_S'] = 1. / self.fs_old  # settings for sampling frequency
                self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

            if f_unit == "Hz":
                f_S_scale = 1.
            elif f_unit == "kHz":
                f_S_scale = 1.e3
            elif f_unit == "MHz":
                f_S_scale = 1.e6
            elif f_unit == "GHz":
                f_S_scale = 1.e9
            else:
                logger.warning("Unknown frequency unit {0}".format(f_unit))

            f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$"
            t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$"

        if f_unit == "k":
            plt_f_unit = "f_S"
        else:
            plt_f_unit = f_unit

        fb.fil[0].update({'f_S_scale':
                          f_S_scale})  # scale factor for f_S (Hz, kHz, ...)
        fb.fil[0].update({'freq_specs_unit': f_unit})  # frequency unit
        # time and frequency unit as string e.g. for plot axis labeling
        fb.fil[0].update({"plt_fUnit": plt_f_unit})
        fb.fil[0].update({"plt_tUnit": self.t_units[idx]})
        # complete plot axis labels including unit and arrow
        fb.fil[0].update({"plt_fLabel": f_label})
        fb.fil[0].update({"plt_tLabel": t_label})

        self._freq_range(
            emit=False)  # update f_lim setting without emitting signal

        self.emit({'view_changed': 'f_S'})

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

    def eventFilter(self, source, event):
        """
        Filter all events generated by the QLineEdit `f_S` widget. 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 with full precision (only if `spec_edited`== True) and
          display the stored value in selected format. Emit 'view_changed':'f_S'
        """
        def _store_entry():
            """
            Update filter dictionary, set line edit entry with reduced precision
            again.
            """
            if self.spec_edited:
                fb.fil[0].update({'f_S_prev': fb.fil[0]['f_S']})
                fb.fil[0].update({
                    'f_S':
                    safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos')
                })
                fb.fil[0].update({'T_S': 1. / fb.fil[0]['f_S']})
                fb.fil[0].update({'f_max': fb.fil[0]['f_S']})

                self._freq_range(emit=False)  # update plotting range
                self.emit({'view_changed': 'f_S'})
                self.spec_edited = False  # reset flag, changed entry has been saved

        if source.objectName() == 'f_S':
            if event.type() == QEvent.FocusIn:
                self.spec_edited = False
                source.setText(str(fb.fil[0]['f_S']))  # full precision
            elif event.type() == QEvent.KeyPress:
                self.spec_edited = True  # entry has been changed
                key = event.key()
                if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}:
                    _store_entry()
                elif key == QtCore.Qt.Key_Escape:  # revert changes
                    self.spec_edited = False
                    source.setText(str(fb.fil[0]['f_S']))  # full precision

            elif event.type() == QEvent.FocusOut:
                _store_entry()
                source.setText(params['FMT'].format(
                    fb.fil[0]['f_S']))  # reduced prec.
        # Call base class method to continue normal event processing:
        return super(FreqUnits, self).eventFilter(source, event)

    # -------------------------------------------------------------
    def _freq_range(self, emit=True):
        """
        Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S
        or for double-sided spectrum between -f_S/2 and f_S/2

        Emit 'view_changed':'f_range' when `emit=True`
        """
        if type(emit) == int:  # signal was emitted by combobox
            emit = True

        rangeType = qget_cmb_box(self.cmbFRange)

        fb.fil[0].update({'freqSpecsRangeType': rangeType})
        f_max = fb.fil[0]["f_max"]

        if rangeType == 'whole':
            f_lim = [0, f_max]
        elif rangeType == 'sym':
            f_lim = [-f_max / 2., f_max / 2.]
        else:
            f_lim = [0, f_max / 2.]

        fb.fil[0]['freqSpecsRange'] = f_lim  # store settings in dict

        if emit:
            self.emit({'view_changed': 'f_range'})

    # -------------------------------------------------------------
    def load_dict(self):
        """
        Reload comboBox settings and textfields from filter dictionary
        Block signals during update of combobox / lineedit widgets
        """
        self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

        self.cmbUnits.blockSignals(True)
        idx = self.cmbUnits.findText(
            fb.fil[0]['freq_specs_unit'])  # get and set
        self.cmbUnits.setCurrentIndex(idx)  # index for freq. unit combo box
        self.cmbUnits.blockSignals(False)

        self.cmbFRange.blockSignals(True)
        idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType'])
        self.cmbFRange.setCurrentIndex(idx)  # set frequency range
        self.cmbFRange.blockSignals(False)

        self.butSort.blockSignals(True)
        self.butSort.setChecked(fb.fil[0]['freq_specs_sort'])
        self.butSort.blockSignals(False)

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

    def _store_sort_flag(self):
        """
        Store sort flag in filter dict and emit 'specs_changed':'f_sort'
        when sort button is checked.
        """
        fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked()
        if self.butSort.isChecked():
            self.emit({'specs_changed': 'f_sort'})
Esempio n. 2
0
class FreqUnits(QWidget):
    """
    Build and update widget for entering the frequency units
    
    The following key-value pairs of the `fb.fil[0]` dict are modified:

        - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string
        - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency
                               values for labelling the frequency axis
        - `'f_S'` : The sampling frequency for referring frequency values to as a float
        - `'f_max'` : maximum frequency for scaling frequency axis              
        - `'plt_fUnit'`: frequency unit as string
        - `'plt_tUnit'`: time unit as string
        - `'plt_fLabel'`: label for frequency axis
        - `'plt_tLabel'`: label for time axis

    """

    # class variables (shared between instances if more than one exists)
    sig_tx = pyqtSignal(object)  # outgoing

    def __init__(self, parent, title="Frequency Units"):

        super(FreqUnits, self).__init__(parent)
        self.title = title
        self.spec_edited = False  # flag whether QLineEdit field has been edited

        self._construct_UI()

    def _construct_UI(self):
        """
        Construct the User Interface
        """
        self.layVMain = QVBoxLayout()  # Widget main layout

        f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz']
        self.t_units = ['', '', '', 's', 'ms', r'$\mu$s', 'ns']

        bfont = QFont()
        bfont.setBold(True)

        self.lblUnits = QLabel(self)
        self.lblUnits.setText("Freq. Unit:")
        self.lblUnits.setFont(bfont)

        self.fs_old = fb.fil[0]['f_S']  # store current sampling frequency
        self.ledF_S = QLineEdit()
        self.ledF_S.setText(str(fb.fil[0]["f_S"]))
        self.ledF_S.setObjectName("f_S")
        self.ledF_S.installEventFilter(self)  # filter events

        self.lblF_S = QLabel(self)
        self.lblF_S.setText(to_html("f_S", frmt='bi'))

        self.cmbUnits = QComboBox(self)
        self.cmbUnits.setObjectName("cmbUnits")
        self.cmbUnits.addItems(f_units)
        self.cmbUnits.setToolTip(
            'Select whether frequencies are specified w.r.t. \n'
            'the sampling frequency "f_S", to the Nyquist frequency \n'
            'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S '
            'but plots graphs over the frequency index k.')
        self.cmbUnits.setCurrentIndex(1)
        #        self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))#
        #        self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole)

        fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")]
        self.cmbFRange = QComboBox(self)
        self.cmbFRange.setObjectName("cmbFRange")
        for f in fRanges:
            self.cmbFRange.addItem(f[0], f[1])
        self.cmbFRange.setToolTip("Select frequency range (whole or half).")
        self.cmbFRange.setCurrentIndex(0)

        # Combobox resizes with longest entry
        self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.butSort = QToolButton(self)
        self.butSort.setText("Sort")
        self.butSort.setCheckable(True)
        self.butSort.setChecked(True)
        self.butSort.setToolTip(
            "Sort frequencies in ascending order when pushed.")
        self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}")

        self.layHUnits = QHBoxLayout()
        self.layHUnits.addWidget(self.cmbUnits)
        self.layHUnits.addWidget(self.cmbFRange)
        self.layHUnits.addWidget(self.butSort)

        # Create a gridLayout consisting of QLabel and QLineEdit fields
        # for setting f_S, the units and the actual frequency specs:
        self.layGSpecWdg = QGridLayout()  # sublayout for spec fields
        self.layGSpecWdg.addWidget(self.lblF_S, 1, 0)
        self.layGSpecWdg.addWidget(self.ledF_S, 1, 1)
        self.layGSpecWdg.addWidget(self.lblUnits, 0, 0)
        self.layGSpecWdg.addLayout(self.layHUnits, 0, 1)

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

        self.layVMain.addWidget(frmMain)
        self.layVMain.setContentsMargins(*params['wdg_margins'])

        self.setLayout(self.layVMain)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.cmbUnits.currentIndexChanged.connect(self.update_UI)
        self.cmbFRange.currentIndexChanged.connect(self._freq_range)
        self.butSort.clicked.connect(self._store_sort_flag)
        #----------------------------------------------------------------------

        self.update_UI()  # first-time initialization

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

    def update_UI(self):
        """
        Transform the displayed frequency spec input fields according to the units
        setting. 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!
        Signals are blocked before changing the value for f_S programmatically

        update_UI is called
        - during init
        - when the unit combobox is changed

        Finally, store freqSpecsRange and emit 'view_changed' signal via _freq_range
        """
        idx = self.cmbUnits.currentIndex()  # read index of units combobox
        f_unit = str(self.cmbUnits.currentText())  # and the label

        self.ledF_S.setVisible(f_unit not in {"f_S", "f_Ny",
                                              "k"})  # only vis. when
        self.lblF_S.setVisible(f_unit not in {"f_S", "f_Ny",
                                              "k"})  # not normalized
        f_S_scale = 1  # default setting for f_S scale

        if f_unit in {"f_S", "f_Ny", "k"}:  # normalized frequency
            self.fs_old = fb.fil[0]['f_S']  # store current sampling frequency

            if f_unit == "f_S":  # normalized to f_S
                fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1.
                f_label = r"$F = f\, /\, f_S = \Omega \, /\,  2 \mathrm{\pi} \; \rightarrow$"
            elif f_unit == "f_Ny":  # idx == 1: normalized to f_nyq = f_S / 2
                fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2.
                f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$"
            else:
                fb.fil[0]['f_S'] = 1
                fb.fil[0]['f_max'] = params['N_FFT']
                f_label = r"$k \; \rightarrow$"
            t_label = r"$n \; \rightarrow$"

            self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

        else:  # Hz, kHz, ...
            if fb.fil[0]['freq_specs_unit'] in {"f_S", "f_Ny",
                                                "k"}:  # previous setting
                fb.fil[0]['f_S'] = fb.fil[0][
                    'f_max'] = self.fs_old  # restore prev. sampling frequency
                self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

            if f_unit == "Hz":
                f_S_scale = 1.
            elif f_unit == "kHz":
                f_S_scale = 1.e3
            elif f_unit == "MHz":
                f_S_scale = 1.e6
            elif f_unit == "GHz":
                f_S_scale = 1.e9
            else:
                logger.warning("Unknown frequency unit {0}".format(f_unit))

            f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$"
            t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$"

        if f_unit == "k":
            plt_f_unit = "f_S"
        else:
            plt_f_unit = f_unit
        fb.fil[0].update({'f_S_scale': f_S_scale})  # scale factor for f_S
        fb.fil[0].update({'freq_specs_unit': f_unit})  # frequency unit
        fb.fil[0].update({"plt_fLabel": f_label})  # label for freq. axis
        fb.fil[0].update({"plt_tLabel": t_label})  # label for time axis
        fb.fil[0].update({"plt_fUnit": plt_f_unit})  # frequency unit as string
        fb.fil[0].update({"plt_tUnit":
                          self.t_units[idx]})  # time unit as string

        self._freq_range(
        )  # update f_lim setting and emit sigUnitChanged signal

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

    def eventFilter(self, source, event):
        """
        Filter all events generated by the QLineEdit 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 with full precision (only if `spec_edited`== True) and
          display the stored value in selected format. Emit 'view_changed':'f_S'
        """
        def _store_entry():
            """
            Update filter dictionary, set line edit entry with reduced precision
            again.
            """
            if self.spec_edited:
                fb.fil[0].update({
                    'f_S':
                    safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos')
                })
                # TODO: ?!
                self._freq_range(emit_sig_range=False)  # update plotting range
                self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_S'})
                self.spec_edited = False  # reset flag, changed entry has been saved

        if source.objectName() == 'f_S':
            if event.type() == QEvent.FocusIn:
                self.spec_edited = False
                source.setText(str(fb.fil[0]['f_S']))  # full precision
            elif event.type() == QEvent.KeyPress:
                self.spec_edited = True  # entry has been changed
                key = event.key()
                if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}:
                    _store_entry()
                elif key == QtCore.Qt.Key_Escape:  # revert changes
                    self.spec_edited = False
                    source.setText(str(fb.fil[0]['f_S']))  # full precision

            elif event.type() == QEvent.FocusOut:
                _store_entry()
                source.setText(params['FMT'].format(
                    fb.fil[0]['f_S']))  # reduced precision
        # Call base class method to continue normal event processing:
        return super(FreqUnits, self).eventFilter(source, event)

    #-------------------------------------------------------------
    def _freq_range(self, emit_sig_range=True):
        """
        Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S
        or for double-sided spectrum between -f_S/2 and f_S/2 and emit
        'view_changed':'f_range'.
        """
        rangeType = qget_cmb_box(self.cmbFRange)

        fb.fil[0].update({'freqSpecsRangeType': rangeType})
        f_max = fb.fil[0]["f_max"]

        if rangeType == 'whole':
            f_lim = [0, f_max]
        elif rangeType == 'sym':
            f_lim = [-f_max / 2., f_max / 2.]
        else:
            f_lim = [0, f_max / 2.]

        fb.fil[0]['freqSpecsRange'] = f_lim  # store settings in dict

        self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_range'})

    #-------------------------------------------------------------
    def load_dict(self):
        """
        Reload comboBox settings and textfields from filter dictionary
        Block signals during update of combobox / lineedit widgets
        """
        self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S']))

        self.cmbUnits.blockSignals(True)
        idx = self.cmbUnits.findText(
            fb.fil[0]['freq_specs_unit'])  # get and set
        self.cmbUnits.setCurrentIndex(idx)  # index for freq. unit combo box
        self.cmbUnits.blockSignals(False)

        self.cmbFRange.blockSignals(True)
        idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType'])
        self.cmbFRange.setCurrentIndex(idx)  # set frequency range
        self.cmbFRange.blockSignals(False)

        self.butSort.blockSignals(True)
        self.butSort.setChecked(fb.fil[0]['freq_specs_sort'])
        self.butSort.blockSignals(False)

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

    def _store_sort_flag(self):
        """
        Store sort flag in filter dict and emit 'specs_changed':'f_sort'
        when sort button is checked.
        """
        fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked()
        if self.butSort.isChecked():
            self.sig_tx.emit({'sender': __name__, 'specs_changed': 'f_sort'})
Esempio n. 3
0
class Input_Fixpoint_Specs(QWidget):
    """
    Create the widget that holds the dynamically loaded fixpoint filter ui
    """

    # sig_resize = pyqtSignal()  # emit a signal when the image has been resized
    sig_rx_local = pyqtSignal(object)  # incoming from subwidgets -> process_sig_rx_local
    sig_rx = pyqtSignal(object)  # incoming, connected to input_tab_widget.sig_rx
    sig_tx = pyqtSignal(object)  # outcgoing
    from pyfda.libs.pyfda_qt_lib import emit

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

        self.tab_label = 'Fixpoint'
        self.tool_tip = ("<span>Select a fixpoint implementation for the filter,"
                         " simulate it or generate a Verilog netlist.</span>")
        self.parent = parent
        self.fx_path = os.path.realpath(
            os.path.join(dirs.INSTALL_DIR, 'fixpoint_widgets'))

        self.no_fx_filter_img = os.path.join(self.fx_path, "no_fx_filter.png")
        if not os.path.isfile(self.no_fx_filter_img):
            logger.error("Image {0:s} not found!".format(self.no_fx_filter_img))

        self.default_fx_img = os.path.join(self.fx_path, "default_fx_img.png")
        if not os.path.isfile(self.default_fx_img):
            logger.error("Image {0:s} not found!".format(self.default_fx_img))

        self._construct_UI()
        inst_wdg_list = self._update_filter_cmb()
        if len(inst_wdg_list) == 0:
            logger.warning("No fixpoint filter found for this type of filter!")
        else:
            logger.debug("Imported {0:d} fixpoint filters:\n{1}"
                         .format(len(inst_wdg_list.split("\n"))-1, inst_wdg_list))
        self._update_fixp_widget()

# ------------------------------------------------------------------------------
    def process_sig_rx_local(self, dict_sig: dict = None) -> None:
        """
        Process signals coming in from input and output quantizer subwidget and the
        dynamically instantiated subwidget and emit {'fx_sim': 'specs_changed'} in
        the end.
        """
        if dict_sig['id'] == id(self):
            logger.warning(f'RX_LOCAL - Stopped infinite loop: "{first_item(dict_sig)}"')
            return

        elif 'fx_sim' in dict_sig and dict_sig['fx_sim'] == 'specs_changed':
            self.wdg_dict2ui()  # update wordlengths in UI and set RUN button to 'changed'
            dict_sig.update({'id': id(self)})  # propagate 'specs_changed' with self 'id'
            self.emit(dict_sig)
            return

        # ---- Process input and output quantizer settings ('ui' in dict_sig) --
        elif 'ui' in dict_sig:
            if 'wdg_name' not in dict_sig:
                logger.warning(f"No key 'wdg_name' in dict_sig:\n{pprint_log(dict_sig)}")
                return

            elif dict_sig['wdg_name'] == 'w_input':
                """
                Input fixpoint format has been changed or butLock has been clicked.
                When I/O lock is active, copy input fixpoint word format to output
                word format.
                """
                if dict_sig['ui'] == 'butLock'\
                        and not self.wdg_w_input.butLock.isChecked():
                    # butLock was deactivitated, don't do anything
                    return
                elif self.wdg_w_input.butLock.isChecked():
                    # but lock was activated or wordlength setting have been changed
                    fb.fil[0]['fxqc']['QO']['WI'] = fb.fil[0]['fxqc']['QI']['WI']
                    fb.fil[0]['fxqc']['QO']['WF'] = fb.fil[0]['fxqc']['QI']['WF']
                    fb.fil[0]['fxqc']['QO']['W'] = fb.fil[0]['fxqc']['QI']['W']

            elif dict_sig['wdg_name'] == 'w_output':
                """
                Output fixpoint format has been changed. When I/O lock is active, copy
                output fixpoint word format to input word format.
                """
                if self.wdg_w_input.butLock.isChecked():
                    fb.fil[0]['fxqc']['QI']['WI'] = fb.fil[0]['fxqc']['QO']['WI']
                    fb.fil[0]['fxqc']['QI']['WF'] = fb.fil[0]['fxqc']['QO']['WF']
                    fb.fil[0]['fxqc']['QI']['W'] = fb.fil[0]['fxqc']['QO']['W']

            elif dict_sig['wdg_name'] in {'q_output', 'q_input'}:
                pass
            else:
                logger.error("Unknown wdg_name '{0}' in dict_sig:\n{1}"
                             .format(dict_sig['wdg_name'], pprint_log(dict_sig)))
                return

            if dict_sig['ui'] not in {'WI', 'WF', 'ovfl', 'quant', 'cmbW', 'butLock'}:
                logger.warning("Unknown value '{0}' for key 'ui'".format(dict_sig['ui']))

            self.wdg_dict2ui()  # update wordlengths in UI and set RUN button to 'changed'
            self.emit({'fx_sim': 'specs_changed'})  # propagate 'specs_changed'

        else:
            logger.error(f"Unknown key/value in 'dict_sig':\n{pprint_log(dict_sig)}")

# ------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig: dict = None) -> None:
        """
        Process signals coming in via `sig_rx` from other widgets.

        Trigger fx simulation:

        1. ``fx_sim': 'init'``: Start fixpoint simulation by sending
           'fx_sim':'start_fx_response_calculation'

        2. ``fx_sim_calc_response()``: Receive stimulus from widget in
            'fx_sim':'calc_frame_fx_response' and pass it to fixpoint simulation method

        3. Store fixpoint response in `fb.fx_result` and return to initiating routine
        """

        # logger.info(
        #     "SIG_RX(): vis={0}\n{1}".format(self.isVisible(), pprint_log(dict_sig)))
        # logger.debug(f'SIG_RX():  "{first_item(dict_sig)}"')

        if dict_sig['id'] == id(self):
            # logger.warning(f'Stopped infinite loop: "{first_item(dict_sig)}"')
            return

        elif 'data_changed' in dict_sig and dict_sig['data_changed'] == "filter_designed":
            # New filter has been designed, update list of available filter topologies
            self._update_filter_cmb()
            return

        elif 'data_changed' in dict_sig or\
             ('view_changed' in dict_sig and dict_sig['view_changed'] == 'q_coeff'):
            # Filter data has changed (but not the filter type) or the coefficient
            # format / wordlength have been changed in `input_coeffs`. The latter means
            # the view / display has been changed (wordlength) but not the actual
            # coefficients in the `input_coeffs` widget. However, the wordlength setting
            # is copied to the fxqc dict and from there to the fixpoint widget.
            # - update fields in the fixpoint filter widget - wordlength may have
            #   been changed.
            # - Set RUN button to "changed" in wdg_dict2ui()
            self.wdg_dict2ui()

        # --------------- FX Simulation -------------------------------------------
        elif 'fx_sim' in dict_sig:
            if dict_sig['fx_sim'] == 'init':
                # fixpoint simulation has been started externally, e.g. by
                # `impz.impz_init()`, return a handle to the fixpoint filter function
                # via signal-slot connection
                if not self.fx_wdg_found:
                    logger.error("No fixpoint widget found!")
                    qstyle_widget(self.butSimFx, "error")
                    self.emit({'fx_sim': 'error'})
                elif self.fx_sim_init() != 0:  # returned an error
                    qstyle_widget(self.butSimFx, "error")
                    self.emit({'fx_sim': 'error'})
                else:
                    self.emit({'fx_sim': 'start_fx_response_calculation',
                               'fxfilter_func': self.fx_filt_ui.fxfilter})

            elif dict_sig['fx_sim'] == 'calc_frame_fx_response':
                self.fx_sim_calc_response(dict_sig)
                # return to the routine collecting the response frame by frame
                return

            elif dict_sig['fx_sim'] == 'specs_changed':
                # fixpoint specification have been changed somewhere, update ui
                # and set run button to "changed" in wdg_dict2ui()
                self.wdg_dict2ui()
            elif dict_sig['fx_sim'] == 'finish':
                qstyle_widget(self.butSimFx, "normal")
            else:
                logger.error('Unknown "fx_sim" command option "{0}"\n'
                             '\treceived from "{1}".'
                             .format(dict_sig['fx_sim'], dict_sig['class']))

        # ---- resize image when "Fixpoint" tab is selected or widget size is changed:
        elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] in {'resized', 'tab'}\
                and self.isVisible():
            self.resize_img()

# ------------------------------------------------------------------------------
    def _construct_UI(self) -> None:
        """
        Intitialize the main GUI, consisting of:

        - A combo box to select the filter topology and an image of the topology

        - The input quantizer

        - The UI of the fixpoint filter widget

        - Simulation and export buttons
        """
# ------------------------------------------------------------------------------
        # Define frame and layout for the dynamically updated filter widget
        # The actual filter widget is instantiated in self.set_fixp_widget() later on

        self.layH_fx_wdg = QHBoxLayout()
        # self.layH_fx_wdg.setContentsMargins(*params['wdg_margins'])
        frmHDL_wdg = QFrame(self)
        frmHDL_wdg.setLayout(self.layH_fx_wdg)
        # frmHDL_wdg.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

# ------------------------------------------------------------------------------
#       Initialize fixpoint filter combobox, title and description
# ------------------------------------------------------------------------------
        self.cmb_fx_wdg = QComboBox(self)
        self.cmb_fx_wdg.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.lblTitle = QLabel("not set", self)
        self.lblTitle.setWordWrap(True)
        self.lblTitle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        layHTitle = QHBoxLayout()
        layHTitle.addWidget(self.cmb_fx_wdg)
        layHTitle.addWidget(self.lblTitle)

        self.frmTitle = QFrame(self)
        self.frmTitle.setLayout(layHTitle)
        self.frmTitle.setContentsMargins(*params['wdg_margins'])

# ------------------------------------------------------------------------------
#       Input and Output Quantizer
# ------------------------------------------------------------------------------
#       - instantiate widgets for input and output quantizer
#       - pass the quantization (sub-?) dictionary to the constructor
# ------------------------------------------------------------------------------

        self.wdg_w_input = UI_W(self, q_dict=fb.fil[0]['fxqc']['QI'],
                                wdg_name='w_input', label='', lock_visible=True)
        self.wdg_w_input.sig_tx.connect(self.process_sig_rx_local)

        cmb_q = ['round', 'floor', 'fix']

        self.wdg_w_output = UI_W(self, q_dict=fb.fil[0]['fxqc']['QO'],
                                 wdg_name='w_output', label='')
        self.wdg_w_output.sig_tx.connect(self.process_sig_rx_local)

        self.wdg_q_output = UI_Q(self, q_dict=fb.fil[0]['fxqc']['QO'],
                                 wdg_name='q_output',
                                 label='Output Format <i>Q<sub>Y&nbsp;</sub></i>:',
                                 cmb_q=cmb_q, cmb_ov=['wrap', 'sat'])
        self.wdg_q_output.sig_tx.connect(self.sig_rx_local)

        if HAS_DS:
            cmb_q.append('dsm')
        self.wdg_q_input = UI_Q(self, q_dict=fb.fil[0]['fxqc']['QI'],
                                wdg_name='q_input',
                                label='Input Format <i>Q<sub>X&nbsp;</sub></i>:',
                                cmb_q=cmb_q)
        self.wdg_q_input.sig_tx.connect(self.sig_rx_local)

        # Layout and frame for input quantization
        layVQiWdg = QVBoxLayout()
        layVQiWdg.addWidget(self.wdg_q_input)
        layVQiWdg.addWidget(self.wdg_w_input)
        frmQiWdg = QFrame(self)
        # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken)
        frmQiWdg.setLayout(layVQiWdg)
        frmQiWdg.setContentsMargins(*params['wdg_margins'])

        # Layout and frame for output quantization
        layVQoWdg = QVBoxLayout()
        layVQoWdg.addWidget(self.wdg_q_output)
        layVQoWdg.addWidget(self.wdg_w_output)
        frmQoWdg = QFrame(self)
        # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken)
        frmQoWdg.setLayout(layVQoWdg)
        frmQoWdg.setContentsMargins(*params['wdg_margins'])

# ------------------------------------------------------------------------------
#       Dynamically updated image of filter topology (label as placeholder)
# ------------------------------------------------------------------------------
        # allow setting background color
        # lbl_fixp_img_palette = QPalette()
        # lbl_fixp_img_palette.setColor(QPalette(window, Qt: white))
        # lbl_fixp_img_palette.setBrush(self.backgroundRole(), QColor(150, 0, 0))
        # lbl_fixp_img_palette.setColor(QPalette: WindowText, Qt: blue)

        self.lbl_fixp_img = QLabel("img not set", self)
        self.lbl_fixp_img.setAutoFillBackground(True)
        # self.lbl_fixp_img.setPalette(lbl_fixp_img_palette)
        # self.lbl_fixp_img.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        self.embed_fixp_img(self.no_fx_filter_img)
        layHImg = QHBoxLayout()
        layHImg.setContentsMargins(0, 0, 0, 0)
        layHImg.addWidget(self.lbl_fixp_img)  # , Qt.AlignCenter)
        self.frmImg = QFrame(self)
        self.frmImg.setLayout(layHImg)
        self.frmImg.setContentsMargins(*params['wdg_margins'])

# ------------------------------------------------------------------------------
#       Simulation and export Buttons
# ------------------------------------------------------------------------------
        self.butExportHDL = QPushButton(self)
        self.butExportHDL.setToolTip(
            "Create Verilog or VHDL netlist for fixpoint filter.")
        self.butExportHDL.setText("Create HDL")

        self.butSimFx = QPushButton(self)
        self.butSimFx.setToolTip("Start fixpoint simulation.")
        self.butSimFx.setText("Sim. FX")

        self.layHHdlBtns = QHBoxLayout()
        self.layHHdlBtns.addWidget(self.butSimFx)
        self.layHHdlBtns.addWidget(self.butExportHDL)
        # This frame encompasses the HDL buttons sim and convert
        frmHdlBtns = QFrame(self)
        # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken)
        frmHdlBtns.setLayout(self.layHHdlBtns)
        frmHdlBtns.setContentsMargins(*params['wdg_margins'])

# -------------------------------------------------------------------
#       Top level layout
# -------------------------------------------------------------------
        splitter = QSplitter(self)
        splitter.setOrientation(Qt.Vertical)
        splitter.addWidget(frmHDL_wdg)
        splitter.addWidget(frmQoWdg)
        splitter.addWidget(self.frmImg)

        # 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, 3000, 5000])

        layVMain = QVBoxLayout()
        layVMain.addWidget(self.frmTitle)
        layVMain.addWidget(frmHdlBtns)
        layVMain.addWidget(frmQiWdg)
        layVMain.addWidget(splitter)
        layVMain.addStretch()
        layVMain.setContentsMargins(*params['wdg_margins'])

        self.setLayout(layVMain)

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        self.sig_rx_local.connect(self.process_sig_rx_local)
        # dynamic connection in `self._update_fixp_widget()`:
        # -----
        # if hasattr(self.fx_filt_ui, "sig_rx"):
        #     self.sig_rx.connect(self.fx_filt_ui.sig_rx)
        # if hasattr(self.fx_filt_ui, "sig_tx"):
        #     self.fx_filt_ui.sig_tx.connect(self.sig_rx_local)
        # ----
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.cmb_fx_wdg.currentIndexChanged.connect(self._update_fixp_widget)
        self.butExportHDL.clicked.connect(self.exportHDL)
        self.butSimFx.clicked.connect(lambda x: self.emit({'fx_sim': 'start'}))
        # ----------------------------------------------------------------------
        # EVENT FILTER
        # ----------------------------------------------------------------------
        # # monitor events and generate sig_resize event when resized
        # self.lbl_fixp_img.installEventFilter(self)
        # # ... then redraw image when resized
        # self.sig_resize.connect(self.resize_img)

# ------------------------------------------------------------------------------
    def _update_filter_cmb(self) -> str:
        """
        (Re-)Read list of available fixpoint filters for a given filter design
        every time a new filter design is selected.

        Then try to import the fixpoint designs in the list and populate the
        fixpoint implementation combo box `self.cmb_fx_wdg` when successfull.

        Returns
        -------
        inst_wdg_str: str
          string with all fixpoint widgets that could be instantiated successfully
        """
        inst_wdg_str = ""  # full names of successfully instantiated widgets for logging
        # remember last fx widget setting:
        last_fx_wdg = qget_cmb_box(self.cmb_fx_wdg, data=False)
        self.cmb_fx_wdg.clear()
        fc = fb.fil[0]['fc']

        if 'fix' in fb.filter_classes[fc]:
            self.cmb_fx_wdg.blockSignals(True)
            for class_name in fb.filter_classes[fc]['fix']:  # get class name
                try:   # construct module + class name ...
                    mod_class_name = fb.fixpoint_classes[class_name]['mod'] + '.'\
                        + class_name
                    # ... and display name
                    disp_name = fb.fixpoint_classes[class_name]['name']
                    self.cmb_fx_wdg.addItem(disp_name, mod_class_name)
                    inst_wdg_str += '\t' + class_name + ' : ' + mod_class_name + '\n'
                except AttributeError as e:
                    logger.warning('Widget "{0}":\n{1}'.format(class_name, e))
                    self.embed_fixp_img(self.no_fx_filter_img)
                    continue  # with next `class_name` of for loop
                except KeyError as e:
                    logger.warning("No fixpoint filter for filter type {0} available."
                                   .format(e))
                    self.embed_fixp_img(self.no_fx_filter_img)
                    continue  # with next `class_name` of for loop

            # restore last fx widget if possible
            idx = self.cmb_fx_wdg.findText(last_fx_wdg)
            # set to idx 0 if not found (returned -1)
            self.cmb_fx_wdg.setCurrentIndex(max(idx, 0))
            self.cmb_fx_wdg.blockSignals(False)
        else:  # no fixpoint widget
            self.embed_fixp_img(self.no_fx_filter_img)
        self._update_fixp_widget()
        return inst_wdg_str

# # ------------------------------------------------------------------------------
#     def eventFilter(self, source, event):
#         """
#         Filter all events generated by monitored QLabel, only resize events are
#         processed here, generating a `sig_resize` signal. All other events
#         are passed on to the next hierarchy level.
#         """
#         if event.type() == QEvent.Resize:
#             logger.warning("resize event!")
#             self.sig_resize.emit()

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

# ------------------------------------------------------------------------------
    def embed_fixp_img(self, img_file: str) -> QPixmap:
        """
        Embed `img_file` in png format as `self.img_fixp`

        Parameters
        ----------
        img_file: str
            path and file name to image file

        Returns
        -------
        self.img_fixp: QPixmap object
            pixmap containing the passed img_file
        """
        if not os.path.isfile(img_file):
            logger.warning("Image file {0} doesn't exist.".format(img_file))
            img_file = self.default_fx_img

        _, file_extension = os.path.splitext(img_file)
        if file_extension != '.png':
            logger.error('Unknown file extension "{0}"!'.format(file_extension))
            img_file = self.default_fx_img

        self.img_fixp = QPixmap(img_file)
        # logger.warning(f"img_fixp = {img_file}")
        # logger.warning(f"_embed_fixp_img(): {self.img_fixp.__class__.__name__}")
        return self.img_fixp

# ------------------------------------------------------------------------------
    def resize_img(self) -> None:
        """
        Triggered when `self` (the widget) is selected or resized. The method resizes
        the image inside QLabel to completely fill the label while keeping
        the aspect ratio. An offset of some pixels is needed, otherwise the image
        is clipped.
        """
        # logger.warning(f"resize_img(): img_fixp = {self.img_fixp.__class__.__name__}")

        if self.parent is None:  # parent is QApplication, has no width or height
            par_w, par_h = 300, 700  # fixed size for module level test
        else:  # widget parent is InputTabWidget()
            par_w, par_h = self.parent.width(), self.parent.height()

        img_w, img_h = self.img_fixp.width(), self.img_fixp.height()

        if img_w > 10:
            max_h = int(max(np.floor(img_h * par_w/img_w) - 5, 20))
        else:
            max_h = 200
        logger.debug("img size: {0},{1}, frm size: {2},{3}, max_h: {4}"
                     .format(img_w, img_h, par_w, par_h, max_h))

        # The following doesn't work because the width of the parent widget can grow
        # with the image size
        # img_scaled = self.img_fixp.scaled(self.lbl_fixp_img.size(),
        # Qt.KeepAspectRatio, Qt.SmoothTransformation)
        img_scaled = self.img_fixp.scaledToHeight(max_h, Qt.SmoothTransformation)

        self.lbl_fixp_img.setPixmap(img_scaled)

# ------------------------------------------------------------------------------
    def _update_fixp_widget(self):
        """
        This method is called at the initialization of the widget and when
        a new fixpoint filter implementation is selected from the combo box:

        - Destruct old instance of fixpoint filter widget `self.fx_filt_ui`
        - Import and instantiate new fixpoint filter widget e.g. after changing the
          filter topology as
        - Try to load image for filter topology
        - Update the UI of the widget
        - Try to instantiate HDL filter as `self.fx_filt_ui.fixp_filter` with
            dummy data
        - emit {'fx_sim': 'specs_changed'} when successful
        """
        def _disable_fx_wdg(self) -> None:

            if hasattr(self, "fx_filt_ui") and self.fx_filt_ui is not None:
                # is a fixpoint widget loaded?
                try:
                    # try to remove widget from layout
                    self.layH_fx_wdg.removeWidget(self.fx_filt_ui)
                    # delete QWidget when scope has been left
                    self.fx_filt_ui.deleteLater()
                except AttributeError as e:
                    logger.error("Destructing UI failed!\n{0}".format(e))

            self.fx_wdg_found = False
            self.butSimFx.setEnabled(False)
            self.butExportHDL.setVisible(False)
            # self.layH_fx_wdg.setVisible(False)
            self.img_fixp = self.embed_fixp_img(self.no_fx_filter_img)
            self.resize_img()
            self.lblTitle.setText("")

            self.fx_filt_ui = None
        # -----------------------------------------------------------
        _disable_fx_wdg(self)  # destruct old fixpoint widget instance:

        # instantiate new fixpoint widget class as self.fx_filt_ui
        cmb_wdg_fx_cur = qget_cmb_box(self.cmb_fx_wdg, data=False)
        if cmb_wdg_fx_cur:  # at least one valid fixpoint widget found
            self.fx_wdg_found = True
            # get list [module name and path, class name]
            fx_mod_class_name = qget_cmb_box(self.cmb_fx_wdg, data=True).rsplit('.', 1)
            fx_mod = importlib.import_module(fx_mod_class_name[0])  # get module
            fx_filt_ui_class = getattr(fx_mod, fx_mod_class_name[1])  # get class
            logger.info("Instantiating new FX widget\n\t"
                        f"{fx_mod.__name__}.{fx_filt_ui_class.__name__}")
            # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            self.fx_filt_ui = fx_filt_ui_class()  # instantiate the fixpoint widget
            # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            # and add it to layout:
            self.layH_fx_wdg.addWidget(self.fx_filt_ui, stretch=1)
            self.fx_filt_ui.setVisible(True)
            self.wdg_dict2ui()  # initialize the fixpoint subwidgets from the fxqc_dict

            # ---- connect signals to fx_filt_ui ----
            if hasattr(self.fx_filt_ui, "sig_rx"):
                self.sig_rx.connect(self.fx_filt_ui.sig_rx)
            if hasattr(self.fx_filt_ui, "sig_tx"):
                self.fx_filt_ui.sig_tx.connect(self.sig_rx_local)

            # ---- get name of new fixpoint filter image ----
            if not (hasattr(self.fx_filt_ui, "img_name") and self.fx_filt_ui.img_name):
                # no image name defined, use default image
                img_file = self.default_fx_img
            else:
                # get path of imported fixpoint widget ...
                file_path = os.path.dirname(fx_mod.__file__)
                # ... and construct full image name from it
                img_file = os.path.join(file_path, self.fx_filt_ui.img_name)

            # ---- instantiate and scale graphic of filter topology ----
            self.embed_fixp_img(img_file)
            self.resize_img()

            # ---- set title and description for filter
            self.lblTitle.setText(self.fx_filt_ui.title)

            # Check which methods the fixpoint widget provides and enable
            # corresponding buttons:
            self.butExportHDL.setVisible(hasattr(self.fx_filt_ui, "to_hdl"))
            self.butSimFx.setEnabled(hasattr(self.fx_filt_ui, "fxfilter"))
            self.update_fxqc_dict()
            self.emit({'fx_sim': 'specs_changed'})

# ------------------------------------------------------------------------------
    def wdg_dict2ui(self):
        """
        Trigger an update of the fixpoint widget UI when view (i.e. fixpoint
        coefficient format) or data have been changed outside this class. Additionally,
        pass the fixpoint quantization widget to update / restore other subwidget
        settings.

        Set the RUN button to "changed".
        """
#        fb.fil[0]['fxqc']['QCB'].update({'scale':(1 << fb.fil[0]['fxqc']['QCB']['W'])})
        self.wdg_q_input.dict2ui(fb.fil[0]['fxqc']['QI'])
        self.wdg_q_output.dict2ui(fb.fil[0]['fxqc']['QO'])
        self.wdg_w_input.dict2ui(fb.fil[0]['fxqc']['QI'])
        self.wdg_w_output.dict2ui(fb.fil[0]['fxqc']['QO'])
        if self.fx_wdg_found and hasattr(self.fx_filt_ui, "dict2ui"):
            self.fx_filt_ui.dict2ui()
#            dict_sig = {'fx_sim':'specs_changed'}
#            self.emit(dict_sig)

        qstyle_widget(self.butSimFx, "changed")

# ------------------------------------------------------------------------------
    def update_fxqc_dict(self):
        """
        Update the fxqc dictionary before simulation / HDL generation starts.
        """
        if self.fx_wdg_found:
            # get a dict with the coefficients and fixpoint settings from fixpoint widget
            if hasattr(self.fx_filt_ui, "ui2dict"):
                fb.fil[0]['fxqc'].update(self.fx_filt_ui.ui2dict())
                logger.debug("update fxqc: \n{0}".format(pprint_log(fb.fil[0]['fxqc'])))
        else:
            logger.error("No fixpoint widget found!")

# ------------------------------------------------------------------------------
    def exportHDL(self):
        """
        Synthesize HDL description of filter
        """
        dlg = QFD(self)  # instantiate file dialog object

        file_types = "Verilog (*.v)"
        # needed for overwrite confirmation when name is entered without suffix:
        dlg.setDefaultSuffix('v')
        dlg.setWindowTitle('Export Verilog')
        dlg.setNameFilter(file_types)
        dlg.setDirectory(dirs.save_dir)
        # set mode "save file" instead "open file":
        dlg.setAcceptMode(QFD.AcceptSave)
        dlg.setOption(QFD.DontConfirmOverwrite, False)
        if dlg.exec_() == QFD.Accepted:
            hdl_file = qstr(dlg.selectedFiles()[0])
            # hdl_type = extract_file_ext(qstr(dlg.selectedNameFilter()))[0]

# =============================================================================
#       # static method getSaveFileName_() is simple but unflexible
#         hdl_file, hdl_filter = dlg.getSaveFileName_(
#                 caption="Save Verilog netlist as (this also defines the module name)",
#                 directory=dirs.save_dir, filter=file_types)
#         hdl_file = qstr(hdl_file)
#         if hdl_file != "": # "operation cancelled" returns an empty string
#             # return '.v' or '.vhd' depending on filetype selection:
#             # hdl_type = extract_file_ext(qstr(hdl_filter))[0]
#             # sanitized dir + filename + suffix. The filename suffix is replaced
#             # by `v` later.
#             hdl_file = os.path.normpath(hdl_file) # complete path + file name
# =============================================================================
            hdl_dir_name = os.path.dirname(hdl_file)  # extract the directory path
            if not os.path.isdir(hdl_dir_name):  # create directory if it doesn't exist
                os.mkdir(hdl_dir_name)
            dirs.save_dir = hdl_dir_name  # make this directory the new default / base dir
            hdl_file_name = os.path.splitext(os.path.basename(hdl_file))[0]
            hdl_full_name = os.path.join(hdl_dir_name, hdl_file_name + ".v")
            # remove all non-alphanumeric chars:
            vlog_mod_name = re.sub(r'\W+', '', hdl_file_name).lower()

            logger.info('Creating hdl_file "{0}"\n\twith top level module "{1}"'
                        .format(hdl_full_name, vlog_mod_name))
            try:
                self.update_fxqc_dict()
                self.fx_filt_ui.construct_fixp_filter()
                code = self.fx_filt_ui.to_hdl(name=vlog_mod_name)
                # logger.info(str(code)) # print verilog code to console
                with io.open(hdl_full_name, 'w', encoding="utf8") as f:
                    f.write(str(code))

                logger.info("HDL conversion finished!")
            except (IOError, TypeError) as e:
                logger.warning(e)

    # --------------------------------------------------------------------------
    def fx_sim_init(self):
        """
        Initialize fix-point simulation:

        - Update the `fxqc_dict` containing all quantization information
        - Setup a filter instance for fixpoint simulation
        - Request a stimulus signal

        Returns
        -------
        error: int
            0 for sucessful fx widget construction, -1 for error
        """
        try:
            self.update_fxqc_dict()
            self.fx_filt_ui.init_filter()   # setup filter instance
            return 0

        except ValueError as e:
            logger.error('Fixpoint stimulus generation failed during "init"'
                         '\nwith "{0} "'.format(e))
        return -1

# ------------------------------------------------------------------------------
    def fx_sim_calc_response(self, dict_sig) -> None:
        """
        - Read fixpoint stimulus from `dict_sig` in integer format
        - Pass it to the fixpoint filter which calculates the fixpoint response
        - Store the result in `fb.fx_results` and return. In case of an error,
          `fb.fx_results == None`

        Returns
        -------
        None
        """
        try:
            # logger.info(
            #     'Simulate fixpoint frame with "{0}" stimulus:\n\t{1}'.format(
            #         dict_sig['class'],
            #         pprint_log(dict_sig['fx_stimulus'], tab=" "),
            #         ))

            # Run fixpoint simulation and store the results as integer values:
            fb.fx_results = self.fx_filt_ui.fxfilter(dict_sig['fx_stimulus'])

            if len(fb.fx_results) == 0:
                logger.error("Fixpoint simulation returned empty results!")
            # else:
            #     # logger.debug("fx_results: {0}"\
            #     #            .format(pprint_log(fb.fx_results, tab= " ")))
            #     logger.info(
            #         f'Fixpoint simulation successful for dict\n{pprint_log(dict_sig)}'
            #         f'\tStimuli: Shape {np.shape(dict_sig["fx_stimulus"])}'
            #         f' of type "{dict_sig["fx_stimulus"].dtype}"'
            #         f'\n\tResponse: Shape {np.shape(fb.fx_results)}'
            #         f' of type "{type(fb.fx_results).__name__} "'
            #         f' ("{type(fb.fx_results[0]).__name__}")'
            #     )

        except ValueError as e:
            logger.error("Simulator error {0}".format(e))
            fb.fx_results = None

        except AssertionError as e:
            logger.error('Fixpoint simulation failed for dict\n{0}'
                         '\twith msg. "{1}"\n\tStimuli: Shape {2} of type "{3}"'
                         '\n\tResponse: Shape {4} of type "{5}"'.format(
                            pprint_log(dict_sig), e,
                            np.shape(dict_sig['fx_stimulus']),
                            dict_sig['fx_stimulus'].dtype,
                            np.shape(fb.fx_results),
                            type(fb.fx_results)
                                ))
            fb.fx_results = None

        if fb.fx_results is None:
            qstyle_widget(self.butSimFx, "error")
        else:
            pass # everything ok, return 
            # logger.debug("Sending fixpoint results")
        return
Esempio n. 4
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)