Ejemplo n.º 1
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()
Ejemplo n.º 2
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'})
Ejemplo n.º 3
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'})