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

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

    #    sig_tx = pyqtSignal(object) # outgoing from process_signals

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

        self._construct_UI()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Polar range limits
        rmin = 0
        rmax = 1

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

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

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

        self.draw()  # initial plot

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

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

        self._save_axes()

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

        self._restore_axes()

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

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

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

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

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

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

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

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

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

        self.draw()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            zlevel = bottom - top_bottom * zlevel_rel

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

            np.seterr(**old_settings_seterr)

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

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

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

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

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

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

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

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

            PN_SIZE = 8  # size of P/N symbols

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

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

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

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

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

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

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

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

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

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

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

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

        self.redraw()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 2
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_signals() )
    sig_rx = pyqtSignal(object)
#    sig_tx = pyqtSignal(object) # outgoing from process_signals

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

    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.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkLinphase = QCheckBox("Zero phase", self)
        self.chkLinphase.setToolTip("<span>Subtract linear phase according to 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("Show 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")

        #----------------------------------------------------------------------
        #               ### 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.chkLinphase)
        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.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.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

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

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_signals)
        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.cmbUnitsA.currentIndexChanged.connect(self.draw)
        self.cmbShowH.currentIndexChanged.connect(self.draw)

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

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

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
                
#------------------------------------------------------------------------------
    def process_signals(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\
                     .format(dict_sig, self.needs_draw, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\
                    or 'home' in dict_sig or self.needs_draw:
                self.draw()
                self.needs_draw = False
                self.needs_redraw = False
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\
                    or self.needs_redraw:
                self.redraw()
                self.needs_redraw = False
            elif 'view_changed' in dict_sig:
                self.update_view()
        else:
            # TODO: draw wouldn't be necessary for 'view_changed', only update view 
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig or 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.needs_redraw = True

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes
        """
#        self.ax = self.mplwidget.ax
        self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.clear()
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    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_S/2
        F_PB  = self.F_PB
        F_SB  = fb.fil[0]['F_SB'] * self.f_S
        F_SB2 = fb.fil[0]['F_SB2'] * self.f_S
        F_PB2 = fb.fil[0]['F_PB2'] * self.f_S

        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_S - F_lim_upl
                F_lim_lol = self.f_S - F_lim_lol
                F_lim_upc = self.f_S - F_lim_upc
                F_lim_loc = self.f_S - F_lim_loc
                F_lim_upr = self.f_S - F_lim_upr
                F_lim_lor = self.f_S - 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.specs:
                    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
        """
        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)
            nbins = len(self.ax.get_yticks()) # number of ticks on main y-axis
            # 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))
#        else:
#            try:
#                self.mplwidget.fig.delaxes(self.ax_p)
#            except (KeyError, AttributeError):
#                pass
#        self.draw()

#------------------------------------------------------------------------------
    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.calc_hf()
        self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        # 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()

        # Linphase settings only makes sense for amplitude plot
        self.chkLinphase.setCheckable(self.unitA == 'V')
        self.chkLinphase.setEnabled(self.unitA == 'V')

        self.specs = self.chkSpecs.isChecked()
        self.linphase = self.chkLinphase.isChecked()

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

        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_S

        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_S/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.linphase: # 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':
                A_lim = [20*np.log10(A_min) -10, 20*np.log10(1+A_max) +1]
                self.H_plt = 20*np.log10(abs(H))
                H_str += ' in dB ' + r'$\rightarrow$'
            elif self.unitA == 'V': #  'lin'
                A_lim = [0, (1.05 + A_max)]
                self.H_plt = H
                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 $'

            #-----------------------------------------------------------
            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.specs: 
                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)

            self.ax.set_title(r'Magnitude Frequency Response')
            self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
            self.ax.set_ylabel(H_str)

        self.redraw()
        
#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 3
0
class Plot_PZ(QWidget):
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self, parent):
        super(Plot_PZ, self).__init__(parent)
        self.needs_calc = True  # flag whether filter data has been changed
        self.needs_draw = False  # flag whether whether figure needs to be drawn
        # with new limits etc. (not implemented yet)
        self.tool_tip = "Pole / zero plan"
        self.tab_label = "P / Z"

        self._construct_UI()

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

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

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

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.chkHf = QCheckBox("Show |H(f)|", self)
        self.chkHf.setToolTip(
            "<span>Display |H(f)| around unit circle.</span>")
        self.chkHf.setEnabled(True)

        self.chkHfLog = QCheckBox("Log. Scale", self)
        self.chkHfLog.setToolTip("<span>Log. scale for |H(f)|.</span>")
        self.chkHfLog.setEnabled(True)

        self.diaRad_Hf = QDial(self)
        self.diaRad_Hf.setRange(2., 10.)
        self.diaRad_Hf.setValue(2)
        self.diaRad_Hf.setTracking(False)  # produce less events when turning
        self.diaRad_Hf.setFixedHeight(30)
        self.diaRad_Hf.setFixedWidth(30)
        self.diaRad_Hf.setWrapping(False)
        self.diaRad_Hf.setToolTip(
            "<span>Set max. radius for |H(f)| plot.</span>")

        self.lblRad_Hf = QLabel("Radius", self)

        self.chkFIR_P = QCheckBox("Plot FIR Poles", self)
        self.chkFIR_P.setToolTip("<span>Show FIR poles at the origin.</span>")
        self.chkFIR_P.setChecked(True)

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.chkHf)
        layHControls.addWidget(self.chkHfLog)
        layHControls.addWidget(self.diaRad_Hf)
        layHControls.addWidget(self.lblRad_Hf)
        layHControls.addStretch(10)
        layHControls.addWidget(self.chkFIR_P)

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

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self.draw()  # calculate and draw poles and zeros

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.chkHf.clicked.connect(self.draw)
        self.chkHfLog.clicked.connect(self.draw)
        self.diaRad_Hf.valueChanged.connect(self.draw)
        self.chkFIR_P.clicked.connect(self.draw)

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

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

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

    def update_view(self):
        """
        Draw the figure with new limits, scale etcs without recalculating H(f)
        -- not yet implemented, just use draw() for the moment
        """
        self.draw()

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

    def draw(self):
        self.chkFIR_P.setVisible(fb.fil[0]['ft'] == 'FIR')
        self.draw_pz()

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

    def draw_pz(self):
        """
        (re)draw P/Z plot
        """
        p_marker = params['P_Marker']
        z_marker = params['Z_Marker']

        zpk = fb.fil[0]['zpk']

        # add antiCausals if they exist (must take reciprocal to plot)
        if 'rpk' in fb.fil[0]:
            zA = fb.fil[0]['zpk'][0]
            zA = np.conj(1. / zA)
            pA = fb.fil[0]['zpk'][1]
            pA = np.conj(1. / pA)
            zC = np.append(zpk[0], zA)
            pC = np.append(zpk[1], pA)
            zpk[0] = zC
            zpk[1] = pC

        self.ax.clear()

        [z, p, k] = self.zplane(z=zpk[0],
                                p=zpk[1],
                                k=zpk[2],
                                plt_ax=self.ax,
                                plt_poles=self.chkFIR_P.isChecked()
                                or fb.fil[0]['ft'] == 'IIR',
                                mps=p_marker[0],
                                mpc=p_marker[1],
                                mzs=z_marker[0],
                                mzc=z_marker[1])

        self.ax.set_title(r'Pole / Zero Plot')
        self.ax.set_xlabel('Real axis')
        self.ax.set_ylabel('Imaginary axis')

        self.draw_Hf(r=self.diaRad_Hf.value())

        self.redraw()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()

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

    def zplane(self,
               b=None,
               a=1,
               z=None,
               p=None,
               k=1,
               pn_eps=1e-3,
               analog=False,
               plt_ax=None,
               plt_poles=True,
               style='square',
               anaCircleRad=0,
               lw=2,
               mps=10,
               mzs=10,
               mpc='r',
               mzc='b',
               plabel='',
               zlabel=''):
        """
        Plot the poles and zeros in the complex z-plane either from the
        coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False)
        or directly from the zeros and poles (z,p) (zpk = True).

        When only b is given, an FIR filter with all poles at the origin is assumed.

        Parameters
        ----------
        b :  array_like
             Numerator coefficients (transversal part of filter)
             When b is not None, poles and zeros are determined from the coefficients
             b and a

        a :  array_like (optional, default = 1 for FIR-filter)
             Denominator coefficients (recursive part of filter)

        z :  array_like, default = None
             Zeros
             When b is None, poles and zeros are taken directly from z and p

        p :  array_like, default = None
             Poles

        analog : boolean (default: False)
            When True, create a P/Z plot suitable for the s-plane, i.e. suppress
            the unit circle (unless anaCircleRad > 0) and scale the plot for
            a good display of all poles and zeros.

        pn_eps : float (default : 1e-2)
             Tolerance for separating close poles or zeros

        plt_ax : handle to axes for plotting (default: None)
            When no axes is specified, the current axes is determined via plt.gca()

        plt_poles : Boolean (default : True)
            Plot poles. This can be used to suppress poles for FIR systems
            where all poles are at the origin.

        style : string (default: 'square')
            Style of the plot, for style == 'square' make scale of x- and y-
            axis equal.

        mps : integer  (default: 10)
            Size for pole marker

        mzs : integer (default: 10)
            Size for zero marker

        mpc : char (default: 'r')
            Pole marker colour

        mzc : char (default: 'b')
            Zero marker colour

        lw : integer (default:  2)
            Linewidth for unit circle

        plabel, zlabel : string (default: '')
            This string is passed to the plot command for poles and zeros and
            can be displayed by legend()


        Returns
        -------
        z, p, k : ndarray


        Notes
        -----
        """
        # TODO:
        # - polar option
        # - add keywords for color of circle -> **kwargs
        # - add option for multi-dimensional arrays and zpk data

        # make sure that all inputs are arrays
        b = np.atleast_1d(b)
        a = np.atleast_1d(a)
        z = np.atleast_1d(z)  # make sure that p, z  are arrays
        p = np.atleast_1d(p)

        if b.any():  # coefficients were specified
            if len(b) < 2 and len(a) < 2:
                logger.error(
                    'No proper filter coefficients: both b and a are scalars!')
                return z, p, k

            # The coefficients are less than 1, normalize the coefficients
            if np.max(b) > 1:
                kn = np.max(b)
                b = b / float(kn)
            else:
                kn = 1.

            if np.max(a) > 1:
                kd = np.max(a)
                a = a / abs(kd)
            else:
                kd = 1.

            # Calculate the poles, zeros and scaling factor
            p = np.roots(a)
            z = np.roots(b)
            k = kn / kd
        elif not (len(p) or len(z)):  # P/Z were specified
            logger.error('Either b,a or z,p must be specified!')
            return z, p, k

        # find multiple poles and zeros and their multiplicities
        if len(p) < 2:  # single pole, [None] or [0]
            if not p or p == 0:  # only zeros, create equal number of poles at origin
                p = np.array(0, ndmin=1)  #
                num_p = np.atleast_1d(len(z))
            else:
                num_p = [1.]  # single pole != 0
        else:
            #p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg')
            p, num_p = unique_roots(p, tol=pn_eps, rtype='avg')
    #        p = np.array(p); num_p = np.ones(len(p))
        if len(z) > 0:
            z, num_z = unique_roots(z, tol=pn_eps, rtype='avg')

    #        z = np.array(z); num_z = np.ones(len(z))
    #z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg')
        else:
            num_z = []

        ax = plt_ax  #.subplot(111)
        if analog == False:
            # create the unit circle for the z-plane
            uc = patches.Circle((0, 0),
                                radius=1,
                                fill=False,
                                color='grey',
                                ls='solid',
                                zorder=1)
            ax.add_patch(uc)
            if style == 'square':
                #r = 1.1
                #ax.axis([-r, r, -r, r]) # overridden by next option
                ax.axis('equal')
        #    ax.spines['left'].set_position('center')
        #    ax.spines['bottom'].set_position('center')
        #    ax.spines['right'].set_visible(True)
        #    ax.spines['top'].set_visible(True)

        else:  # s-plane
            if anaCircleRad > 0:
                # plot a circle with radius = anaCircleRad
                uc = patches.Circle((0, 0),
                                    radius=anaCircleRad,
                                    fill=False,
                                    color='grey',
                                    ls='solid',
                                    zorder=1)
                ax.add_patch(uc)
            # plot real and imaginary axis
            ax.axhline(lw=2, color='k', zorder=1)
            ax.axvline(lw=2, color='k', zorder=1)

        # Plot the zeros
        ax.scatter(z.real,
                   z.imag,
                   s=mzs * mzs,
                   zorder=2,
                   marker='o',
                   facecolor='none',
                   edgecolor=mzc,
                   lw=lw,
                   label=zlabel)
        # and print their multiplicity
        for i in range(len(z)):
            logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i]))
            if num_z[i] > 1:
                ax.text(np.real(z[i]),
                        np.imag(z[i]),
                        '  (' + str(num_z[i]) + ')',
                        va='top',
                        color=mzc)
        if plt_poles:
            # Plot the poles
            ax.scatter(p.real,
                       p.imag,
                       s=mps * mps,
                       zorder=2,
                       marker='x',
                       color=mpc,
                       lw=lw,
                       label=plabel)
            # and print their multiplicity
            for i in range(len(p)):
                logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i]))
                if num_p[i] > 1:
                    ax.text(np.real(p[i]),
                            np.imag(p[i]),
                            '  (' + str(num_p[i]) + ')',
                            va='bottom',
                            color=mpc)

# =============================================================================
#            # increase distance between ticks and labels
#            # to give some room for poles and zeros
#         for tick in ax.get_xaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#         for tick in ax.get_yaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#
# =============================================================================
        xl = ax.get_xlim()
        Dx = max(abs(xl[1] - xl[0]), 0.05)
        yl = ax.get_ylim()
        Dy = max(abs(yl[1] - yl[0]), 0.05)
        ax.set_xlim((xl[0] - Dx * 0.05, max(xl[1] + Dx * 0.05, 0)))
        ax.set_ylim((yl[0] - Dy * 0.05, yl[1] + Dy * 0.05))

        return z, p, k

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

    def draw_Hf(self, r=2):
        """
        Draw the magnitude frequency response around the UC
        """
        # suppress "divide by zero in log10" warnings
        old_settings_seterr = np.seterr()
        np.seterr(divide='ignore')

        self.chkHfLog.setVisible(self.chkHf.isChecked())
        self.diaRad_Hf.setVisible(self.chkHf.isChecked())
        self.lblRad_Hf.setVisible(self.chkHf.isChecked())
        if not self.chkHf.isChecked():
            return
        ba = fb.fil[0]['ba']
        w, H = sig.freqz(ba[0], ba[1], worN=params['N_FFT'], whole=True)
        H = np.abs(H)
        if self.chkHfLog.isChecked():
            H = np.clip(np.log10(H), -6, None)  # clip to -120 dB
            H = H - np.max(H)  # shift scale to H_min ... 0
            H = 1 + (r - 1) * (1 + H / abs(np.min(H)))  # scale to 1 ... r
        else:
            H = 1 + (r - 1) * H / np.max(H)  #  map |H(f)| to a range 1 ... r
        y = H * np.sin(w)
        x = H * np.cos(w)

        self.ax.plot(x, y, label="|H(f)|")
        uc = patches.Circle((0, 0),
                            radius=r,
                            fill=False,
                            color='grey',
                            ls='dashed',
                            zorder=1)
        self.ax.add_patch(uc)

        xl = self.ax.get_xlim()
        xmax = max(abs(xl[0]), abs(xl[1]), r * 1.05)
        yl = self.ax.get_ylim()
        ymax = max(abs(yl[0]), abs(yl[1]), r * 1.05)
        self.ax.set_xlim((-xmax, xmax))
        self.ax.set_ylim((-ymax, ymax))

        np.seterr(**old_settings_seterr)
Exemplo n.º 4
0
class PlotPZ(QWidget):

    def __init__(self, parent): 
        super(PlotPZ, self).__init__(parent)
        self._construct_UI()
        
    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.chkHf = QCheckBox("Show |H(f)|", self)
        self.chkHf.setToolTip("<span>Enable display of |H(f)|.</span>")
        self.chkHf.setEnabled(True)


        layHControls = QHBoxLayout()
        layHControls.addWidget(self.chkHf)
        layHControls.addStretch(10)
        
        #----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        #----------------------------------------------------------------------
        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layHControls)

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets 
        #----------------------------------------------------------------------  
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)


        # make this the central widget, taking all available space:
 #       self.setCentralWidget(self.mplwidget)
        
        self.init_axes()

        self.draw() # calculate and draw poles and zeros

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
        self.chkHf.clicked.connect(self.draw)
#------------------------------------------------------------------------------
    @pyqtSlot(object)
    def process_signals(self, sig_dict):
        """
        Process signals coming from the navigation toolbar
        """
        if 'update_view' in sig_dict:
            self.update_view()
        elif 'enabled' in sig_dict:
            self.enable_ui(sig_dict['enabled'])
        elif 'home' in sig_dict:
            self.draw()
        else:
            pass

#------------------------------------------------------------------------------
    def enable_ui(self, enabled):
        """
        Triggered when the toolbar is enabled or disabled
        """
        # self.frmControls.setEnabled(enabled) # no control widgets yet
        if enabled:
            self.draw()

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes
        """
        if self.chkHf.isChecked():
            self.ax = self.mplwidget.fig.add_subplot(111)
        else:
            self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.clear()
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etcs without recalculating H(f)
        -- not yet implemented, just use draw() for the moment
        """
        self.draw()

#------------------------------------------------------------------------------
    def draw(self):
        if self.mplwidget.mplToolbar.enabled:
            self.init_axes()
            self.draw_pz()
            
#------------------------------------------------------------------------------
    def draw_pz(self):
        """
        (re)draw P/Z plot
        """
        p_marker = params['P_Marker']
        z_marker = params['Z_Marker']
        
        zpk = fb.fil[0]['zpk']

        # add antiCausals if they exist (must take reciprocal to plot)
        if 'rpk' in fb.fil[0]:
            zA = fb.fil[0]['zpk'][0]
            zA = np.conj(1./zA)
            pA = fb.fil[0]['zpk'][1]
            pA = np.conj(1./pA)
            zC = np.append(zpk[0],zA)
            pC = np.append(zpk[1],pA)
            zpk[0] = zC
            zpk[1] = pC

        self.ax.clear()

        [z,p,k] = self.zplane(z = zpk[0], p = zpk[1], k = zpk[2], plt_ax = self.ax,
            mps = p_marker[0], mpc = p_marker[1], mzs = z_marker[0], mzc = z_marker[1])

        self.ax.set_title(r'Pole / Zero Plot')
        self.ax.set_xlabel('Real axis')
        self.ax.set_ylabel('Imaginary axis')

        if self.chkHf.isChecked():
            self.draw_Hf()

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()

#------------------------------------------------------------------------------
    def zplane(self, b=None, a=1, z=None, p=None, k =1,  pn_eps=1e-3, analog=False,
              plt_ax = None, style='square', anaCircleRad=0, lw=2,
              mps = 10, mzs = 10, mpc = 'r', mzc = 'b', plabel = '', zlabel = ''):
        """
        Plot the poles and zeros in the complex z-plane either from the
        coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False)
        or directly from the zeros and poles (z,p) (zpk = True).
    
        When only b is given, an FIR filter with all poles at the origin is assumed.
    
        Parameters
        ----------
        b :  array_like
             Numerator coefficients (transversal part of filter)
             When b is not None, poles and zeros are determined from the coefficients
             b and a
    
        a :  array_like (optional, default = 1 for FIR-filter)
             Denominator coefficients (recursive part of filter)
             
        z :  array_like, default = None
             Zeros
             When b is None, poles and zeros are taken directly from z and p
    
        p :  array_like, default = None
             Poles
    
        analog : boolean (default: False)
            When True, create a P/Z plot suitable for the s-plane, i.e. suppress
            the unit circle (unless anaCircleRad > 0) and scale the plot for
            a good display of all poles and zeros.
    
        pn_eps : float (default : 1e-2)
             Tolerance for separating close poles or zeros
             
        plt_ax : handle to axes for plotting (default: None)
            When no axes is specified, the current axes is determined via plt.gca()
    
        pltLib :  string (default: 'matplotlib')
             Library for plotting the P/Z plane. Currently, only matplotlib is
             implemented. When pltLib = 'none' or when matplotlib is not
             available, only pass the poles / zeros and their multiplicity
    
        style : string (default: 'square')
            Style of the plot, for style == 'square' make scale of x- and y-
            axis equal.
    
        mps : integer  (default: 10)
            Size for pole marker
            
        mzs : integer (default: 10)
            Size for zero marker
            
        mpc : char (default: 'r')
            Pole marker colour
            
        mzc : char (default: 'b')
            Zero marker colour
            
        lw : integer (default:  2)
            Linewidth for unit circle
    
        plabel, zlabel : string (default: '')
            This string is passed to the plot command for poles and zeros and
            can be displayed by legend()
    
    
        Returns
        -------
        z, p, k : ndarray
    
    
        Notes
        -----
    
        """
        # TODO:
        # - polar option
        # - add keywords for color of circle -> **kwargs
        # - add option for multi-dimensional arrays and zpk data
    
        # make sure that all inputs are arrays
        b = np.atleast_1d(b) 
        a = np.atleast_1d(a)
        z = np.atleast_1d(z) # make sure that p, z  are arrays
        p = np.atleast_1d(p)
    
        if b.any(): # coefficients were specified
            if len(b) < 2 and len(a) < 2:
                logger.error('No proper filter coefficients: both b and a are scalars!')
                return z, p, k
            
            # The coefficients are less than 1, normalize the coefficients
            if np.max(b) > 1:
                kn = np.max(b)
                b = b / float(kn) 
            else:
                kn = 1.
    
            if np.max(a) > 1:
                kd = np.max(a)
                a = a / abs(kd)
            else:
                kd = 1.
    
            # Calculate the poles, zeros and scaling factor
            p = np.roots(a)
            z = np.roots(b)
            k = kn/kd
        elif not (len(p) or len(z)): # P/Z were specified
            logger.error('Either b,a or z,p must be specified!')
            return z, p, k
  
        # find multiple poles and zeros and their multiplicities
        if len(p) < 2: # single pole, [None] or [0]
            if not p or p == 0: # only zeros, create equal number of poles at origin
                p = np.array(0,ndmin=1) # 
                num_p = np.atleast_1d(len(z))
            else:
                num_p = [1.] # single pole != 0
        else:
            #p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg')
            p, num_p = unique_roots(p, tol = pn_eps, rtype='avg')
    #        p = np.array(p); num_p = np.ones(len(p))
        if len(z) > 0:
            z, num_z = unique_roots(z, tol = pn_eps, rtype='avg')
    #        z = np.array(z); num_z = np.ones(len(z))
            #z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg')
        else:
            num_z = []
    
    
        ax = plt_ax#.subplot(111)
        if analog == False:
            # create the unit circle for the z-plane
            uc = patches.Circle((0,0), radius=1, fill=False,
                                color='grey', ls='solid', zorder=1)
            ax.add_patch(uc)
            if style == 'square':
                r = 1.1
                ax.axis([-r, r, -r, r], 'equal')
                ax.axis('equal')
        #    ax.spines['left'].set_position('center')
        #    ax.spines['bottom'].set_position('center')
        #    ax.spines['right'].set_visible(True)
        #    ax.spines['top'].set_visible(True)
    
        else: # s-plane
            if anaCircleRad > 0:
                # plot a circle with radius = anaCircleRad
                uc = patches.Circle((0,0), radius=anaCircleRad, fill=False,
                                    color='grey', ls='solid', zorder=1)
                ax.add_patch(uc)
            # plot real and imaginary axis
            ax.axhline(lw=2, color = 'k', zorder=1)
            ax.axvline(lw=2, color = 'k', zorder=1)
    
        # Plot the zeros
        ax.scatter(z.real, z.imag, s=mzs*mzs, zorder=2, marker = 'o',
                   facecolor = 'none', edgecolor = mzc, lw = lw, label=zlabel)
        # Plot the poles
        ax.scatter(p.real, p.imag, s=mps*mps, zorder=2, marker='x',
                   color=mpc, lw=lw, label=plabel)
    
         # Print multiplicity of poles / zeros
        for i in range(len(z)):
            logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i]))
            if num_z[i] > 1:
                ax.text(np.real(z[i]), np.imag(z[i]),'  (' + str(num_z[i]) +')',
                                va = 'top', color=mzc)
    
        for i in range(len(p)):
            logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i]))
            if num_p[i] > 1:
                ax.text(np.real(p[i]), np.imag(p[i]), '  (' + str(num_p[i]) +')',
                                va = 'bottom', color=mpc)
    
            # increase distance between ticks and labels
            # to give some room for poles and zeros
        for tick in ax.get_xaxis().get_major_ticks():
            tick.set_pad(12.)
            tick.label1 = tick._get_text1()
        for tick in ax.get_yaxis().get_major_ticks():
            tick.set_pad(12.)
            tick.label1 = tick._get_text1()
    
        xl = ax.get_xlim(); Dx = max(abs(xl[1]-xl[0]), 0.05)
        yl = ax.get_ylim(); Dy = max(abs(yl[1]-yl[0]), 0.05)
        ax.set_xlim((xl[0]-Dx*0.05, max(xl[1]+Dx*0.05,0)))
        ax.set_ylim((yl[0]-Dy*0.05, yl[1] + Dy*0.05))
    
        return z, p, k

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

    def draw_Hf(self, r=2):
        """
        Draw the magnitude frequency response around the UC
        """
        ba = fb.fil[0]['ba']
        w, h = sig.freqz(ba[0], ba[1], whole=True)
        h = np.abs(h)
        h = h / np.max(h) +1 #  map |H(f)| to a range 1 ... 2
        y = h * np.sin(w)
        x = h * np.cos(w)

        self.ax.plot(x,y, label="|H(f)|")
        uc = patches.Circle((0,0), radius=r, fill=False,
                                    color='grey', ls='dashed', zorder=1)
        self.ax.add_patch(uc)

        xl = self.ax.get_xlim()
        xmax = max(abs(xl[0]), abs(xl[1]), r*1.05)
        yl = self.ax.get_ylim()
        ymax = max(abs(yl[0]), abs(yl[1]), r*1.05)
        self.ax.set_xlim((-xmax, xmax))
        self.ax.set_ylim((-ymax, ymax))
Exemplo n.º 5
0
class Plot_Tau_G(QWidget):
    """
    Widget for plotting the group delay
    """
    # incoming, connected in sender widget (locally connected to self.process_signals() )
    sig_rx = pyqtSignal(object)

    #    sig_tx = pyqtSignal(object) # outgoing from process_signals

    def __init__(self, parent):
        super(Plot_Tau_G, self).__init__(parent)
        self.verbose = False  # suppress warnings
        self.needs_draw = True  # flag whether plot needs to be updated
        self.needs_redraw = True  # flag whether plot needs to be redrawn
        self.tool_tip = "Group delay"
        self.tab_label = "tau_g"
        self._construct_UI()

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements (currently commented out)
        """

        # =============================================================================
        # #### not needed at the moment ###
        #         self.chkWarnings = QCheckBox("Enable Warnings", self)
        #         self.chkWarnings.setChecked(False)
        #         self.chkWarnings.setToolTip("Print warnings about singular group delay")
        #
        #         layHControls = QHBoxLayout()
        #         layHControls.addStretch(10)
        #         layHControls.addWidget(self.chkWarnings)
        #
        #         # This widget encompasses all control subwidgets:
        #         self.frmControls = QFrame(self)
        #         self.frmControls.setObjectName("frmControls")
        #         self.frmControls.setLayout(layHControls)
        #
        # =============================================================================
        self.mplwidget = MplWidget(self)
        #        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()
        self.draw()  # initial drawing of tau_g

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_signals)
        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)

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

    def process_signals(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\
                     .format(dict_sig, self.needs_draw, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_draw:
                self.draw()
                self.needs_draw = False
                self.needs_redraw = False
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\
                    or self.needs_redraw:
                self.redraw()
                self.needs_redraw = False
            elif 'view_changed' in dict_sig:
                self.update_view()
        else:
            if 'data_changed' in dict_sig or 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig[
                    'ui_changed'] == 'resized':
                self.needs_redraw = True

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

    def init_axes(self):
        """
        Initialize and clear the axes
        """
        self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.clear()
        self.ax.get_xaxis().tick_bottom()  # remove axis ticks on top
        self.ax.get_yaxis().tick_left()  # remove axis ticks right

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

    def calc_tau_g(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        bb = fb.fil[0]['ba'][0]
        aa = fb.fil[0]['ba'][1]

        # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.tau_g = grpdelay(
            bb, aa, params['N_FFT'], whole=True,
            verbose=self.verbose)  # self.chkWarnings.isChecked())

        # Zero phase filters have no group delay (Causal+AntiCausal)
        if 'baA' in fb.fil[0]:
            self.tau_g = np.zeros(self.tau_g.size)

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

    def draw(self):
        self.calc_tau_g()
        self.update_view()

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

    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        #========= select frequency range to be displayed =====================
        #=== shift, scale and select: W -> F, H_cplx -> H_c
        f_S2 = fb.fil[0]['f_S'] / 2.
        F = self.W * f_S2 / np.pi

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

        #================ Main Plotting Routine =========================
        #===  clear the axes and (re)draw the plot

        if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $'
        else:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\
                + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $'
            tau_g = tau_g / fb.fil[0]['f_S']

        #---------------------------------------------------------
        self.ax.clear()  # need to clear, doesn't overwrite
        line_tau_g, = self.ax.plot(F, tau_g, label="Group Delay")
        #---------------------------------------------------------

        self.ax.set_title(r'Group Delay $ \tau_g$')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(tau_str)
        # widen y-limits to suppress numerical inaccuracies when tau_g = constant
        self.ax.set_ylim([max(min(tau_g) - 0.5, 0), max(tau_g) + 0.5])
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 6
0
class Plot_tau_g(QWidget):
    """
    Widget for plotting the group delay
    """
    # incoming, connected in sender widget (locally connected to self.process_signals() )
    sig_rx = pyqtSignal(object)

    #    sig_tx = pyqtSignal(object) # outgoing from process_signals

    def __init__(self):
        super().__init__()
        self.verbose = False  # suppress warnings
        self.algorithm = "auto"
        self.needs_calc = True  # flag whether plot needs to be recalculated
        self.tool_tip = self.tr("Group delay")
        self.tab_label = "\U0001D70F(f)"  # "tau_g" \u03C4

        self.cmb_algorithm_items =\
            ["<span>Select algorithm for calculating the group delay.</span>",
             ("auto", "Auto", "<span>Try to find best-suited algorithm.</span>"),
             ("scipy", "Scipy", "<span>Scipy algorithm.</span>"),
             ("jos", "JOS", "<span>J.O. Smith's algorithm.</span>"),
             ("shpak", "Shpak", "<span>Shpak's algorithm for SOS and other IIR"
              "filters.</span>"),
             ("diff", "Diff", "<span>Textbook-style, differentiate the phase."
              "</span>")
             ]

        self._construct_UI()

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.chkWarnings = QCheckBox(self.tr("Verbose"), self)
        self.chkWarnings.setChecked(self.verbose)
        self.chkWarnings.setToolTip(
            self.
            tr("<span>Print messages about singular group delay and calculation times."
               "</span>"))

        self.cmbAlgorithm = QComboBox(self)
        qcmb_box_populate(self.cmbAlgorithm, self.cmb_algorithm_items,
                          self.algorithm)

        layHControls = QHBoxLayout()
        layHControls.addStretch(10)
        layHControls.addWidget(self.chkWarnings)
        # layHControls.addWidget(self.chkScipy)
        layHControls.addWidget(self.cmbAlgorithm)

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

        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins'])
        self.mplwidget.mplToolbar.a_he.setEnabled(True)
        self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g.html"
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()
        self.draw()  # initial drawing of tau_g

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.cmbAlgorithm.currentIndexChanged.connect(self.draw)

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

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

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

    def init_axes(self):
        """
        Initialize the axes and set some stuff that is not cleared by
        `ax.clear()` later on.
        """
        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 calc_tau_g(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        bb = fb.fil[0]['ba'][0]
        aa = fb.fil[0]['ba'][1]

        # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi:
        # scipy: self.W, self.tau_g = group_delay((bb, aa), w=params['N_FFT'],
        #                                           whole = True)

        if fb.fil[0]['creator'][0] == 'sos':  # one of 'sos', 'zpk', 'ba'
            self.W, self.tau_g = group_delay(
                fb.fil[0]['sos'],
                nfft=params['N_FFT'],
                sos=True,
                whole=True,
                verbose=self.chkWarnings.isChecked(),
                alg=self.cmbAlgorithm.currentData())
        else:
            self.W, self.tau_g = group_delay(
                bb,
                aa,
                nfft=params['N_FFT'],
                whole=True,
                verbose=self.chkWarnings.isChecked(),
                alg=self.cmbAlgorithm.currentData())
            #                                   self.chkWarnings.isChecked())

        # Zero phase filters have no group delay (Causal+AntiCausal)
        if 'baA' in fb.fil[0]:
            self.tau_g = np.zeros(self.tau_g.size)

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

    def draw(self):
        self.calc_tau_g()
        self.update_view()

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

    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        # ========= select frequency range to be displayed =====================
        # === shift, scale and select: W -> F, H_cplx -> H_c
        f_max_2 = fb.fil[0]['f_max'] / 2.
        F = self.W * f_max_2 / np.pi

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

        # ================ Main Plotting Routine =========================
        # ===  clear the axes and (re)draw the plot

        if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $'
        else:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\
                + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $'
            tau_g = tau_g / fb.fil[0]['f_S']

        # ---------------------------------------------------------
        self.ax.clear()  # need to clear, doesn't overwrite
        line_tau_g, = self.ax.plot(F, tau_g, label="Group Delay")
        # ---------------------------------------------------------

        self.ax.xaxis.set_minor_locator(
            AutoMinorLocator())  # enable minor ticks
        self.ax.yaxis.set_minor_locator(
            AutoMinorLocator())  # enable minor ticks
        self.ax.set_title(r'Group Delay $ \tau_g$')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(tau_str)
        # widen y-limits to suppress numerical inaccuracies when tau_g = constant
        self.ax.set_ylim(
            [max(np.nanmin(tau_g) - 0.5, 0),
             np.nanmax(tau_g) + 0.5])
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 7
0
class PlotImpz(QWidget):
    """
    Construct a widget for plotting impulse and general transient responses
    """
    def __init__(self, parent):
        super(PlotImpz, self).__init__(parent)

        self.ACTIVE_3D = False
        self.ui = PlotImpz_UI(self) # create the UI part with buttons etc.
        
        # initial settings for line edit widgets
        self.f1 = self.ui.f1
        self.f2 = self.ui.f2

        self._construct_UI()

    def _construct_UI(self):
        """
        Create the top level UI of the widget, consisting of matplotlib widget
        and control frame.
        """
        
        #----------------------------------------------------------------------
        # mplwidget
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.ui)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        # frequency widgets require special handling as they are scaled with f_s
        self.ui.ledFreq1.installEventFilter(self)
        self.ui.ledFreq2.installEventFilter(self)

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) # connect to toolbar
        self.ui.sig_tx.connect(self.process_signals) # connect to widgets and signals upstream

        self.draw() # initial calculation and drawing

#------------------------------------------------------------------------------
    @pyqtSlot(object)
    def process_signals(self, sig_dict):
        """
        Process signals coming from the navigation toolbar
        """
        logger.debug("processing {0}".format(sig_dict))
        if 'home' in sig_dict  or 'view_changed' in sig_dict:
            self.update_view()
        elif 'enabled' in sig_dict:
            self.enable_ui(sig_dict['enabled'])
        elif 'data_changed' in sig_dict or 'specs_changed' in sig_dict:
                # changing of f_s has to update the plot - more differentiation needed
            self.draw()
        else:
            logger.debug("{0}: dict {1} passed thru".format(__name__, sig_dict))

#------------------------------------------------------------------------------
    def enable_ui(self, enabled):
        """
        Triggered when the toolbar is enabled or disabled
        """
        self.ui.frmControls.setEnabled(enabled)
        if enabled:
            # self.init_axes() # called by self.draw
            self.draw()

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

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

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

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

                self.spec_edited = False # reset flag
                self.draw()

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

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

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

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

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

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

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

#------------------------------------------------------------------------------
    def init_axes(self):
        # clear the axes and (re)draw the plot
        #
        for ax in self.mplwidget.fig.get_axes():
            self.mplwidget.fig.delaxes(ax)

        num_subplots = 0 + (self.ui.plt_time != "None")\
                        + (self.cmplx and self.ui.plt_time in {"Response", "Both"})\
                        + (self.ui.plt_freq != "None")

        if num_subplots > 0:
            self.mplwidget.fig.subplots_adjust(hspace = 0.5)
    
            if self.ui.plt_time != "None":
                self.ax_r = self.mplwidget.fig.add_subplot(num_subplots,1 ,1)
                self.ax_r.clear()
                self.ax_r.get_xaxis().tick_bottom() # remove axis ticks on top
                self.ax_r.get_yaxis().tick_left() # remove axis ticks right
    
            if self.cmplx and self.ui.plt_time in {"Response", "Both"}:
                self.ax_i = self.mplwidget.fig.add_subplot(num_subplots, 1, 2, sharex = self.ax_r)
                self.ax_i.clear()
                self.ax_i.get_xaxis().tick_bottom() # remove axis ticks on top
                self.ax_i.get_yaxis().tick_left() # remove axis ticks right
    
            if self.ui.plt_freq != "None":
                self.ax_fft = self.mplwidget.fig.add_subplot(num_subplots, 1, num_subplots)    
                self.ax_fft.clear()

                self.ax_fft.get_xaxis().tick_bottom() # remove axis ticks on top
                self.ax_fft.get_yaxis().tick_left() # remove axis ticks right
    
            if self.ACTIVE_3D: # not implemented / tested yet
                self.ax3d = self.mplwidget.fig.add_subplot(111, projection='3d')

#------------------------------------------------------------------------------
    def calc(self):
        """
        (Re-)calculate stimulus x[n] and filter response y[n]
        """
        self.n = np.arange(self.ui.N_end)
        self.t = self.n / fb.fil[0]['f_S']

        # calculate stimuli x[n] ==============================================
        if self.ui.stim == "Pulse":
            self.x = np.zeros(self.ui.N_end)
            self.x[0] = self.ui.A1 # create dirac impulse as input signal
            self.title_str = r'Impulse Response'
            self.H_str = r'$h[n]$' # default

        elif self.ui.stim == "Step":
            self.x = self.ui.A1 * np.ones(self.ui.N_end) # create step function
            self.title_str = r'Filter Step Response'
            self.H_str = r'$h_{\epsilon}[n]$'
            
        elif self.ui.stim == "StepErr":
            self.x = self.ui.A1 * np.ones(self.ui.N_end) # create step function
            self.title_str = r'Settling Error'
            self.H_str = r'$h_{\epsilon, \infty} - h_{\epsilon}[n]$'
            
        elif self.ui.stim == "Cos":
            self.x = self.ui.A1 * np.cos(2 * np.pi * self.n * self.f1) +\
                self.ui.A2 * np.cos(2 * np.pi * self.n * self.f2 + self.ui.phi2)
            self.title_str = r'Filter Response to Cosine Signal'
            self.H_str = r'$y_{\cos}[n]$'
                
        elif self.ui.stim == "Sine":
            self.x = self.ui.A1 * np.sin(2 * np.pi * self.n * self.f1 + self.ui.phi1) +\
                self.ui.A2 * np.sin(2 * np.pi * self.n * self.f2 + self.ui.phi2)
            self.title_str = r'Filter Response to Sinusoidal Signal'
            self.H_str = r'$y_{\sin}[n]$'
            
        elif self.ui.stim == "Rect":
            self.x = self.ui.A1 * np.sign(np.sin(2 * np.pi * self.n * self.f1))
            self.title_str = r'Filter Response to Rect. Signal'
            self.H_str = r'$y_{rect}[n]$'

        elif self.ui.stim == "Saw":
            self.x = self.ui.A1 * sig.sawtooth(self.n * self.f1 * 2*np.pi)
            self.title_str = r'Filter Response to Sawtooth Signal'
            self.H_str = r'$y_{saw}[n]$'

        else:
            logger.error('Unknown stimulus "{0}"'.format(self.ui.stim))
            return
        
        # Add noise to stimulus
        if self.ui.noise == "gauss":
            self.x[self.ui.N_start:] += self.ui.noi * np.random.randn(self.ui.N)
        elif self.ui.noise == "uniform":
            self.x[self.ui.N_start:] += self.ui.noi * (np.random.rand(self.ui.N)-0.5)

        # Add DC to stimulus when visible / enabled
        if self.ui.ledDC.isVisible:
            self.x += self.ui.DC
        
        # calculate response self.y[n] and self.y_i[n] (for complex case) =====   
        self.bb = np.asarray(fb.fil[0]['ba'][0])
        self.aa = np.asarray(fb.fil[0]['ba'][1])
        if min(len(self.aa), len(self.bb)) < 2:
            logger.error('No proper filter coefficients: len(a), len(b) < 2 !')
            return

        sos = np.asarray(fb.fil[0]['sos'])
        antiCausal = 'zpkA' in fb.fil[0]
        causal     = not (antiCausal)

        if len(sos) > 0 and (causal): # has second order sections and is causal
            y = sig.sosfilt(sos, self.x)
        elif (antiCausal):
            y = sig.filtfilt(self.bb, self.aa, self.x, -1, None)
        else: # no second order sections or antiCausals for current filter
            y = sig.lfilter(self.bb, self.aa, self.x)

        if self.ui.stim == "StepErr":
            dc = sig.freqz(self.bb, self.aa, [0]) # DC response of the system
            y = y - abs(dc[1]) # subtract DC (final) value from response

        y = np.real_if_close(y, tol = 1e3)  # tol specified in multiples of machine eps
        self.cmplx = np.any(np.iscomplex(y))
        if self.cmplx:
            self.y_i = y.imag
            self.y = y.real
        else:
            self.y = y
            self.y_i = None

        # calculate FFT of stimulus / response
#        if self.ui.plt_freq in {"Stimulus", "Both"}:
        x_win = self.x[self.ui.N_start:self.ui.N_end] * self.ui.win
        self.X = np.abs(np.fft.fft(x_win)) / self.ui.N

#        if self.ui.plt_freq in {"Response", "Both"}:
        y_win = y[self.ui.N_start:self.ui.N_end] * self.ui.win
        self.Y = np.abs(np.fft.fft(y_win)) / self.ui.N

#------------------------------------------------------------------------------
    def update_view(self):
        """
        place holder; should update only the limits without recalculating
        the impulse respons
        """
        self.draw_impz()

#------------------------------------------------------------------------------
    def draw(self):
        """
        Recalculate response and redraw it
        """
        if self.mplwidget.mplToolbar.enabled:
            self.calc()
            self.draw_impz()

#------------------------------------------------------------------------------
    def draw_impz(self):
        """
        (Re-)draw the figure
        """
        f_unit = fb.fil[0]['freq_specs_unit']
        if f_unit in {"f_S", "f_Ny"}:
            unit_frmt = "i" # italic
        else:
            unit_frmt = None
        self.ui.lblFreqUnit1.setText(to_html(f_unit, frmt=unit_frmt))
        self.ui.lblFreqUnit2.setText(to_html(f_unit, frmt=unit_frmt))
        N_start = self.ui.N_start
        self.load_fs()
        self.init_axes()
        
        #================ Main Plotting Routine =========================
        if self.ui.chkMarker.isChecked():
            mkfmt_r = 'o'
            mkfmt_i = 'd'
        else:
            mkfmt_r = mkfmt_i = ' '
        if self.cmplx:           
            H_i_str = r'$\Im\{$' + self.H_str + '$\}$ in V'
            H_str = r'$\Re\{$' + self.H_str + '$\}$ in V'
        else:
            H_str = self.H_str + 'in V'

        if self.ui.chkLog.isChecked():
            H_str = r'$|$ ' + H_str + '$|$ in dBV'
            y = np.maximum(20 * np.log10(abs(self.y)), self.ui.bottom)
            if self.cmplx:
                y_i = np.maximum(20 * np.log10(abs(self.y_i)), self.ui.bottom)
                H_i_str = r'$\log$ ' + H_i_str + ' in dBV'
        else:
            self.ui.bottom = 0
            y = self.y
            y_i = self.y_i

        if self.ui.plt_time in {"Response", "Both"}:
            [ml, sl, bl] = self.ax_r.stem(self.t[N_start:], y[N_start:], 
                bottom=self.ui.bottom, markerfmt=mkfmt_r, label = '$y[n]$')

        if self.ui.plt_time in {"Stimulus", "Both"}:
            stem_fmt = params['mpl_stimuli']
            [ms, ss, bs] = self.ax_r.stem(self.t[N_start:], self.x[N_start:], 
                bottom=self.ui.bottom, label = 'Stim.', **stem_fmt)
            ms.set_mfc(stem_fmt['mfc'])
            ms.set_mec(stem_fmt['mec'])
            ms.set_ms(stem_fmt['ms'])
            ms.set_alpha(stem_fmt['alpha'])
            for stem in ss:
                stem.set_linewidth(stem_fmt['lw'])
                stem.set_color(stem_fmt['mec'])
                stem.set_alpha(stem_fmt['alpha'])
            bs.set_visible(False) # invisible bottomline

        if self.cmplx and self.ui.plt_time in {"Response", "Both"}:
            [ml_i, sl_i, bl_i] = self.ax_i.stem(self.t[N_start:], y_i[N_start:],
                bottom=self.ui.bottom, markerfmt=mkfmt_i, label = '$y_i[n]$')
            self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel'])
            # self.ax_r.get_xaxis().set_ticklabels([]) # removes both xticklabels
            # plt.setp(ax_r.get_xticklabels(), visible=False) 
            # is shorter but imports matplotlib, set property directly instead:
            [label.set_visible(False) for label in self.ax_r.get_xticklabels()]
            self.ax_r.set_ylabel(H_str + r'$\rightarrow $')
            self.ax_i.set_ylabel(H_i_str + r'$\rightarrow $')
        else:
            self.ax_r.set_xlabel(fb.fil[0]['plt_tLabel'])
            self.ax_r.set_ylabel(H_str + r'$\rightarrow $')
        
        self.ax_r.set_title(self.title_str)
        self.ax_r.set_xlim([self.t[N_start],self.t[self.ui.N_end-1]])
        expand_lim(self.ax_r, 0.02)

        # plot frequency domain =========================================
        if self.ui.plt_freq != "None":
            plt_response = self.ui.plt_freq in {"Response","Both"}
            plt_stimulus = self.ui.plt_freq in {"Stimulus","Both"}
            if plt_response and not plt_stimulus:
                XY_str = r'$|Y(\mathrm{e}^{\mathrm{j} \Omega})|$'
            elif not plt_response and plt_stimulus:
                XY_str = r'$|X(\mathrm{e}^{\mathrm{j} \Omega})|$'
            else:
                XY_str = r'$|X,Y(\mathrm{e}^{\mathrm{j} \Omega})|$'
            F = np.fft.fftfreq(self.ui.N, d = 1. / fb.fil[0]['f_S'])

            if plt_stimulus:
                X = self.X.copy()/np.sqrt(2) # enforce deep copy and convert to RMS
                self.Px = np.sum(np.square(self.X))
                if fb.fil[0]['freqSpecsRangeType'] == 'half':
                    X[1:] = 2 * X[1:] # correct for single-sided spectrum (except DC)
            if plt_response:
                Y = self.Y.copy()/np.sqrt(2) # enforce deep copy and convert to RMS
                self.Py = np.sum(np.square(self.Y))
                if fb.fil[0]['freqSpecsRangeType'] == 'half':
                    Y[1:] = 2 * Y[1:] # correct for single-sided spectrum (except DC)

            if self.ui.chkLogF.isChecked():
                unit = unit_P = "dBW"
                unit_nenbw = "dB"
                nenbw = 10 * np.log10(self.ui.nenbw)
                if plt_stimulus:
                    X = np.maximum(20 * np.log10(X), self.ui.bottom_f)
                    self.Px = 10*np.log10(self.Px)
                if plt_response:
                    Y = np.maximum(20 * np.log10(Y), self.ui.bottom_f)
                    self.Py = 10*np.log10(self.Py)
            else:
                unit = "Vrms"
                unit_P = "W"
                unit_nenbw = "bins"
                nenbw = self.ui.nenbw

            XY_str = XY_str + ' in ' + unit

            if fb.fil[0]['freqSpecsRangeType'] == 'sym':
            # shift X, Y and F by f_S/2
                if plt_response:
                    Y = np.fft.fftshift(Y)
                if plt_stimulus:
                    X = np.fft.fftshift(X)
                F = np.fft.fftshift(F)
            elif fb.fil[0]['freqSpecsRangeType'] == 'half':
                # only use the first half of X, Y and F
                if plt_response:
                    Y = Y[0:self.ui.N//2]
                if plt_stimulus:
                    X = X[0:self.ui.N//2]
                F = F[0:self.ui.N//2]
            else: # fb.fil[0]['freqSpecsRangeType'] == 'whole'
                # plot for F = 0 ... 1
                F = np.fft.fftshift(F) + fb.fil[0]['f_S']/2.

            handles = []
            labels = []
            if plt_stimulus:
                h, = self.ax_fft.plot(F, X, color =(0.5,0.5,0.5,0.5), lw=2)
                handles.append(h)
                labels.append("$P_X$ = {0:.3g} {1}".format(self.Px, unit_P))
            if plt_response:
                h, = self.ax_fft.plot(F, Y)
                handles.append(h)
                labels.append("$P_Y$ = {0:.3g} {1}".format(self.Py, unit_P))
                
            labels.append("$NENBW$ = {0:.4g} {1}".format(nenbw, unit_nenbw))
            labels.append("$CGAIN$  = {0:.4g}".format(self.ui.scale))
            handles.append(mpl_patches.Rectangle((0, 0), 1, 1, fc="white",ec="white", lw=0))
            handles.append(mpl_patches.Rectangle((0, 0), 1, 1, fc="white",ec="white", lw=0))
            self.ax_fft.legend(handles, labels, loc='best', fontsize = 'small',
                               fancybox=True, framealpha=0.5)
            

            self.ax_fft.set_xlabel(fb.fil[0]['plt_fLabel'])
            self.ax_fft.set_ylabel(XY_str)
            self.ax_fft.set_xlim(fb.fil[0]['freqSpecsRange'])
            if self.ui.plt_time == "None":
                self.ax_fft.set_title(self.title_str) # no time window, print title here
                
            if self.ui.chkLogF.isChecked():
                # create second axis scaled for noise power scale
                self.ax_fft_noise = self.ax_fft.twinx()
                self.ax_fft_noise.is_twin = True

                corr = 10*np.log10(self.ui.N / self.ui.nenbw) 
                mn, mx = self.ax_fft.get_ylim()
                self.ax_fft_noise.set_ylim(mn+corr, mx+corr)
                self.ax_fft_noise.set_ylabel(r'$P_N$ in dBW')

        if self.ACTIVE_3D: # not implemented / tested yet
            # plotting the stems
            for i in range(N_start, self.ui.N_end):
              self.ax3d.plot([self.t[i], self.t[i]], [y[i], y[i]], [0, y_i[i]],
                             '-', linewidth=2, alpha=.5)

            # plotting a circle on the top of each stem
            self.ax3d.plot(self.t[N_start:], y[N_start:], y_i[N_start:], 'o', markersize=8,
                           markerfacecolor='none', label='$y[n]$')

            self.ax3d.set_xlabel('x')
            self.ax3d.set_ylabel('y')
            self.ax3d.set_zlabel('z')

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
        if hasattr(self, "ax2_fft"):
            self.ax2_fft.grid(False)
Exemplo n.º 8
0
class Plot_FFT_win(QMainWindow):
    """
    Create a pop-up widget for displaying time and frequency view of an FFT 
    window.
    
    Data is passed via the dictionary `win_dict` that is passed during construction.
    """
    # incoming
    sig_rx = pyqtSignal(object)
    # outgoing
    sig_tx = pyqtSignal(object)

    def __init__(self, parent, win_dict_name="win_fft", sym=True):
        super(Plot_FFT_win, self).__init__(parent)

        self.needs_calc = True
        self.needs_draw = True

        self.bottom_f = -80  # min. value for dB display
        self.bottom_t = -60
        self.N = 128  # initial number of data points

        self.pad = 8  # amount of zero padding

        self.win_dict = fb.fil[0][win_dict_name]
        self.sym = sym

        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowTitle('pyFDA Window Viewer')
        self._construct_UI()

        qwindow_stay_on_top(self, True)

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

    def closeEvent(self, event):
        """
        Catch closeEvent (user has tried to close the window) and send a 
        signal to parent where window closing is registered before actually
        closing the window.
        """
        self.sig_tx.emit({'sender': __name__, 'closeEvent': ''})
        event.accept()

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

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | visible = {1}"\
                     .format(dict_sig, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc:
                self.draw()
                self.needs_calc = False
                self.needs_draw = False
            elif 'view_changed' in dict_sig or self.needs_draw:
                self.update_view()
                self.needs_draw = False
            elif ('ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized')\
                or self.needs_redraw:
                self.redraw()
        else:
            if 'data_changed' in dict_sig:
                self.needs_calc = True
            elif 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig[
                    'ui_changed'] == 'resized':
                self.needs_redraw = True

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.chk_auto_N = QCheckBox(self)
        self.chk_auto_N.setChecked(False)
        self.chk_auto_N.setToolTip(
            "Use number of points from calling routine.")

        self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i'))

        self.led_N = QLineEdit(self)
        self.led_N.setText(str(self.N))
        self.led_N.setMaximumWidth(70)
        self.led_N.setToolTip("<span>Number of window data points.</span>")

        self.chk_log_t = QCheckBox("Log", self)
        self.chk_log_t.setChecked(False)
        self.chk_log_t.setToolTip("Display in dB")

        self.led_log_bottom_t = QLineEdit(self)
        self.led_log_bottom_t.setText(str(self.bottom_t))
        self.led_log_bottom_t.setMaximumWidth(50)
        self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.led_log_bottom_t.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        self.lbl_log_bottom_t = QLabel("dB", self)
        self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked())

        self.chk_norm_f = QCheckBox("Norm", self)
        self.chk_norm_f.setChecked(True)
        self.chk_norm_f.setToolTip(
            "Normalize window spectrum for a maximum of 1.")

        self.chk_half_f = QCheckBox("Half", self)
        self.chk_half_f.setChecked(True)
        self.chk_half_f.setToolTip(
            "Display window spectrum in the range 0 ... 0.5 f_S.")

        self.chk_log_f = QCheckBox("Log", self)
        self.chk_log_f.setChecked(True)
        self.chk_log_f.setToolTip("Display in dB")

        self.led_log_bottom_f = QLineEdit(self)
        self.led_log_bottom_f.setText(str(self.bottom_f))
        self.led_log_bottom_f.setMaximumWidth(50)
        self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked())
        self.led_log_bottom_f.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        self.lbl_log_bottom_f = QLabel("dB", self)
        self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked())

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.chk_auto_N)
        layHControls.addWidget(self.lbl_auto_N)
        layHControls.addWidget(self.led_N)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_log_t)
        layHControls.addWidget(self.led_log_bottom_t)
        layHControls.addWidget(self.lbl_log_bottom_t)
        layHControls.addStretch(10)
        layHControls.addWidget(self.chk_norm_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_half_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_log_f)
        layHControls.addWidget(self.led_log_bottom_f)
        layHControls.addWidget(self.lbl_log_bottom_f)

        #         self.tblFiltPerf = QTableWidget(self)
        #         self.tblFiltPerf.setAlternatingRowColors(True)
        # #        self.tblFiltPerf.verticalHeader().setVisible(False)
        #         self.tblFiltPerf.horizontalHeader().setHighlightSections(False)
        #         self.tblFiltPerf.horizontalHeader().setFont(bfont)
        #         self.tblFiltPerf.verticalHeader().setHighlightSections(False)
        #         self.tblFiltPerf.verticalHeader().setFont(bfont)

        self.txtInfoBox = QTextBrowser(self)

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

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget: Layout layVMainMpl (VBox) is defined with MplWidget,
        #              additional widgets can be added (like self.frmControls)
        #              The widget encompasses all other widgets.
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])

        #----------------------------------------------------------------------
        #               ### splitter ###
        #
        # This widget encompasses all control subwidgets
        #----------------------------------------------------------------------

        splitter = QSplitter(self)
        splitter.setOrientation(Qt.Vertical)
        splitter.addWidget(self.mplwidget)
        splitter.addWidget(self.txtInfoBox)

        # setSizes uses absolute pixel values, but can be "misused" by specifying values
        # that are way too large: in this case, the space is distributed according
        # to the _ratio_ of the values:
        splitter.setSizes([3000, 1000])

        self.setCentralWidget(splitter)

        #self.setCentralWidget(self.mplwidget)

        #----------------------------------------------------------------------
        #           Set subplots
        #
        self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2)
        self.ax_t = self.ax[0]
        self.ax_f = self.ax[1]

        self.draw()  # initial drawing

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.chk_log_f.clicked.connect(self.update_view)
        self.chk_log_t.clicked.connect(self.update_view)
        self.led_log_bottom_t.editingFinished.connect(self.update_bottom)
        self.led_log_bottom_f.editingFinished.connect(self.update_bottom)

        self.chk_auto_N.clicked.connect(self.draw)
        self.led_N.editingFinished.connect(self.draw)

        self.chk_norm_f.clicked.connect(self.draw)
        self.chk_half_f.clicked.connect(self.update_view)

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

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

    def update_bottom(self):
        """
        Update log bottom settings
        """
        self.bottom_t = safe_eval(self.led_log_bottom_t.text(),
                                  self.bottom_t,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_t.setText(str(self.bottom_t))

        self.bottom_f = safe_eval(self.led_log_bottom_f.text(),
                                  self.bottom_f,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_f.setText(str(self.bottom_f))

        self.update_view()
#------------------------------------------------------------------------------

    def calc_win(self):
        """
        (Re-)Calculate the window and its FFT
        """
        self.led_N.setEnabled(not self.chk_auto_N.isChecked())
        if self.chk_auto_N.isChecked():
            self.N = self.win_dict['win_len']
            self.led_N.setText(str(self.N))
        else:
            self.N = safe_eval(self.led_N.text(),
                               self.N,
                               sign='pos',
                               return_type='int')

        self.n = np.arange(self.N)

        self.win = calc_window_function(self.win_dict,
                                        self.win_dict['name'],
                                        self.N,
                                        sym=self.sym)

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

        self.F = fftfreq(self.N * self.pad,
                         d=1. / fb.fil[0]['f_S'])  # use zero padding
        self.Win = np.abs(fft(self.win, self.N * self.pad))

        if self.chk_norm_f.isChecked():
            self.Win /= (self.N / self.scale
                         )  # correct gain for periodic signals (coherent gain)
#------------------------------------------------------------------------------

    def draw(self):
        """
        Main entry point:
        Re-calculate \|H(f)\| and draw the figure
        """
        self.calc_win()
        self.update_view()
        self.update_info()

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

    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        self.ax_t.cla()
        self.ax_f.cla()

        self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel'])
        self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$')

        self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$')

        if self.chk_log_t.isChecked():
            self.ax_t.plot(
                self.n,
                np.maximum(20 * np.log10(np.abs(self.win)), self.bottom_t))
        else:
            self.ax_t.plot(self.n, self.win)

        if self.chk_half_f.isChecked():
            F = self.F[:len(self.F * self.pad) // 2]
            Win = self.Win[:len(self.F * self.pad) // 2]
        else:
            F = fftshift(self.F)
            Win = fftshift(self.Win)

        if self.chk_log_f.isChecked():
            self.ax_f.plot(
                F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f))
            nenbw = 10 * np.log10(self.nenbw)
            unit_nenbw = "dB"
        else:
            self.ax_f.plot(F, Win)
            nenbw = self.nenbw
            unit_nenbw = "bins"

        self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked())
        self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked())

        window_name = self.win_dict['name']
        param_txt = ""
        if self.win_dict['n_par'] > 0:
            param_txt = " (" + self.win_dict['par'][1][
                0] + " = {0:.3g})".format(self.win_dict['par'][2][0])
        if self.win_dict['n_par'] > 1:
            param_txt = param_txt[:-1]\
                + ", {0:s} = {1:.3g})".format(self.win_dict['par'][1][1], self.win_dict['par'][2][1])

        self.mplwidget.fig.suptitle(r'{0} Window'.format(window_name) +
                                    param_txt)

        # create two empty patches
        handles = [
            mpl_patches.Rectangle(
                (0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0)
        ] * 2

        labels = []
        labels.append("$NENBW$ = {0:.4g} {1}".format(nenbw, unit_nenbw))
        labels.append("$CGAIN$  = {0:.4g}".format(self.scale))
        self.ax_f.legend(handles,
                         labels,
                         loc='best',
                         fontsize='small',
                         fancybox=True,
                         framealpha=0.7,
                         handlelength=0,
                         handletextpad=0)

        self.redraw()
#------------------------------------------------------------------------------

    def update_info(self):
        """
        Update the text info box for the window
        """
        if 'info' in self.win_dict:
            self.txtInfoBox.setText(self.win_dict['info'])

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 9
0
class Plot_FFT_win(QDialog):
    """
    Create a pop-up widget for displaying time and frequency view of an FFT
    window.

    Data is passed via the dictionary `win_dict` that is passed during construction.
    """
    # incoming
    sig_rx = pyqtSignal(object)
    # outgoing
    sig_tx = pyqtSignal(object)

    def __init__(self,
                 parent,
                 win_dict=fb.fil[0]['win_fft'],
                 sym=True,
                 title='pyFDA Window Viewer'):
        super(Plot_FFT_win, self).__init__(parent)

        self.needs_calc = True
        self.needs_draw = True
        self.needs_redraw = True

        self.bottom_f = -80  # min. value for dB display
        self.bottom_t = -60
        self.N = 32  # initial number of data points
        self.N_auto = win_dict['win_len']

        self.pad = 16  # amount of zero padding

        self.win_dict = win_dict
        self.sym = sym

        self.tbl_rows = 2
        self.tbl_cols = 6
        # initial settings for checkboxes
        self.tbl_sel = [True, True, False, False]

        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowTitle(title)
        self._construct_UI()

        qwindow_stay_on_top(self, True)

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

    def closeEvent(self, event):
        """
        Catch closeEvent (user has tried to close the window) and send a
        signal to parent where window closing is registered before actually
        closing the window.
        """
        self.sig_tx.emit({'sender': __name__, 'closeEvent': ''})
        event.accept()

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

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("PROCESS_SIG_RX - vis: {0}\n{1}"\
                     .format(self.isVisible(), pprint_log(dict_sig)))
        if ('view_changed' in dict_sig and dict_sig['view_changed'] == 'win')\
            or ('filt_changed' in dict_sig and dict_sig['filt_changed'] == 'firwin')\
            or self.needs_calc:
            # logger.warning("Auto: {0} - WinLen: {1}".format(self.N_auto, self.win_dict['win_len']))
            self.N_auto = self.win_dict['win_len']
            self.calc_N()

            if self.isVisible():
                self.draw()
                self.needs_calc = False
            else:
                self.needs_calc = True

        elif 'home' in dict_sig:
            self.update_view()

        else:
            logger.error("Unknown content of dict_sig: {0}".format(dict_sig))

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

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.bfont = QFont()
        self.bfont.setBold(True)

        self.chk_auto_N = QCheckBox(self)
        self.chk_auto_N.setChecked(False)
        self.chk_auto_N.setToolTip(
            "Use number of points from calling routine.")

        self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i'))

        self.led_N = QLineEdit(self)
        self.led_N.setText(str(self.N))
        self.led_N.setMaximumWidth(70)
        self.led_N.setToolTip("<span>Number of window data points.</span>")

        self.chk_log_t = QCheckBox("Log", self)
        self.chk_log_t.setChecked(False)
        self.chk_log_t.setToolTip("Display in dB")

        self.led_log_bottom_t = QLineEdit(self)
        self.led_log_bottom_t.setText(str(self.bottom_t))
        self.led_log_bottom_t.setMaximumWidth(50)
        self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.led_log_bottom_t.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        self.lbl_log_bottom_t = QLabel("dB", self)
        self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked())

        self.chk_norm_f = QCheckBox("Norm", self)
        self.chk_norm_f.setChecked(True)
        self.chk_norm_f.setToolTip(
            "Normalize window spectrum for a maximum of 1.")

        self.chk_half_f = QCheckBox("Half", self)
        self.chk_half_f.setChecked(True)
        self.chk_half_f.setToolTip(
            "Display window spectrum in the range 0 ... 0.5 f_S.")

        self.chk_log_f = QCheckBox("Log", self)
        self.chk_log_f.setChecked(True)
        self.chk_log_f.setToolTip("Display in dB")

        self.led_log_bottom_f = QLineEdit(self)
        self.led_log_bottom_f.setText(str(self.bottom_f))
        self.led_log_bottom_f.setMaximumWidth(50)
        self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked())
        self.led_log_bottom_f.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        self.lbl_log_bottom_f = QLabel("dB", self)
        self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked())

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.chk_auto_N)
        layHControls.addWidget(self.lbl_auto_N)
        layHControls.addWidget(self.led_N)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_log_t)
        layHControls.addWidget(self.led_log_bottom_t)
        layHControls.addWidget(self.lbl_log_bottom_t)
        layHControls.addStretch(10)
        layHControls.addWidget(self.chk_norm_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_half_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.chk_log_f)
        layHControls.addWidget(self.led_log_bottom_f)
        layHControls.addWidget(self.lbl_log_bottom_f)

        self.tblWinProperties = QTableWidget(self.tbl_rows, self.tbl_cols,
                                             self)
        self.tblWinProperties.setAlternatingRowColors(True)
        self.tblWinProperties.verticalHeader().setVisible(False)
        self.tblWinProperties.horizontalHeader().setVisible(False)
        self._init_table(self.tbl_rows, self.tbl_cols, " ")

        self.txtInfoBox = QTextBrowser(self)

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

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget: Layout layVMainMpl (VBox) is defined with MplWidget,
        #              additional widgets can be added (like self.frmControls)
        #              The widget encompasses all other widgets.
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])

        #----------------------------------------------------------------------
        #               ### frmInfo ###
        #
        # This widget encompasses the text info box and the table with window
        # parameters.
        #----------------------------------------------------------------------
        layVInfo = QVBoxLayout(self)
        layVInfo.addWidget(self.tblWinProperties)
        layVInfo.addWidget(self.txtInfoBox)

        self.frmInfo = QFrame(self)
        self.frmInfo.setObjectName("frmInfo")
        self.frmInfo.setLayout(layVInfo)

        #----------------------------------------------------------------------
        #               ### splitter ###
        #
        # This widget encompasses all control subwidgets
        #----------------------------------------------------------------------

        splitter = QSplitter(self)
        splitter.setOrientation(Qt.Vertical)
        splitter.addWidget(self.mplwidget)
        splitter.addWidget(self.frmInfo)

        # setSizes uses absolute pixel values, but can be "misused" by specifying values
        # that are way too large: in this case, the space is distributed according
        # to the _ratio_ of the values:
        splitter.setSizes([3000, 1000])

        layVMain = QVBoxLayout()
        layVMain.addWidget(splitter)
        self.setLayout(layVMain)

        #----------------------------------------------------------------------
        #           Set subplots
        #
        self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2)
        self.ax_t = self.ax[0]
        self.ax_f = self.ax[1]

        self.draw()  # initial drawing

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.chk_log_f.clicked.connect(self.update_view)
        self.chk_log_t.clicked.connect(self.update_view)
        self.led_log_bottom_t.editingFinished.connect(self.update_bottom)
        self.led_log_bottom_f.editingFinished.connect(self.update_bottom)

        self.chk_auto_N.clicked.connect(self.calc_N)
        self.led_N.editingFinished.connect(self.draw)

        self.chk_norm_f.clicked.connect(self.draw)
        self.chk_half_f.clicked.connect(self.update_view)

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.tblWinProperties.itemClicked.connect(self._handle_item_clicked)
#------------------------------------------------------------------------------

    def _init_table(self, rows, cols, val):
        for r in range(rows):
            for c in range(cols):
                item = QTableWidgetItem(val)
                if c % 3 == 0:
                    item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                    if self.tbl_sel[r * 2 + c % 3]:
                        item.setCheckState(Qt.Checked)
                    else:
                        item.setCheckState(Qt.Unchecked)
                self.tblWinProperties.setItem(r, c, item)
#   https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget

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

    def _set_table_item(self, row, col, val, font=None, sel=None):
        """
        Set the table item with the index `row, col` and the value val
        """
        item = self.tblWinProperties.item(row, col)
        item.setText(str(val))

        if font:
            self.tblWinProperties.item(row, col).setFont(font)

        if sel == True:
            item.setCheckState(Qt.Checked)
        if sel == False:
            item.setCheckState(Qt.Unchecked)
        # when sel is not specified, don't change anything

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

    def _handle_item_clicked(self, item):
        if item.column() % 3 == 0:  # clicked on checkbox
            num = item.row() * 2 + item.column() // 3
            if item.checkState() == Qt.Checked:
                self.tbl_sel[num] = True
                logger.debug('"{0}:{1}" Checked'.format(item.text(), num))
            else:
                self.tbl_sel[num] = False
                logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num))

        elif item.column() % 3 == 1:  # clicked on value field
            logger.info("{0:s} copied to clipboard.".format(item.text()))
            fb.clipboard.setText(item.text())

        self.update_view()

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

    def update_bottom(self):
        """
        Update log bottom settings
        """
        self.bottom_t = safe_eval(self.led_log_bottom_t.text(),
                                  self.bottom_t,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_t.setText(str(self.bottom_t))

        self.bottom_f = safe_eval(self.led_log_bottom_f.text(),
                                  self.bottom_f,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_f.setText(str(self.bottom_f))

        self.update_view()

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

    def calc_N(self):
        """
        (Re-)Calculate the number of data points when Auto N chkbox has been
        clicked or when the number of data points has been updated outside this
        class
        """
        if self.chk_auto_N.isChecked():
            self.N = self.N_auto

        self.draw()

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

    def calc_win(self):
        """
        (Re-)Calculate the window and its FFT
        """
        self.led_N.setEnabled(not self.chk_auto_N.isChecked())

        if not self.chk_auto_N.isChecked():
            self.N = safe_eval(self.led_N.text(),
                               self.N,
                               sign='pos',
                               return_type='int')

    # else:
    #self.N = self.win_dict['win_len']

        self.led_N.setText(str(self.N))

        self.n = np.arange(self.N)

        self.win = calc_window_function(self.win_dict,
                                        self.win_dict['name'],
                                        self.N,
                                        sym=self.sym)

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

        self.F = fftfreq(self.N * self.pad,
                         d=1. / fb.fil[0]['f_S'])  # use zero padding
        self.Win = np.abs(fft(self.win, self.N * self.pad))
        if self.chk_norm_f.isChecked():
            self.Win /= (self.N * self.cgain
                         )  # correct gain for periodic signals (coherent gain)

        first_zero = argrelextrema(self.Win[:(self.N * self.pad) // 2],
                                   np.less)
        if np.shape(first_zero)[1] > 0:
            first_zero = first_zero[0][0]
            self.first_zero_f = self.F[first_zero]
            self.sidelobe_level = np.max(
                self.Win[first_zero:(self.N * self.pad) // 2])
        else:
            self.first_zero_f = np.nan
            self.sidelobe_level = 0

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

    def draw(self):
        """
        Main entry point:
        Re-calculate \|H(f)\| and draw the figure
        """
        self.calc_win()
        self.update_view()

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

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

        self.ax_t.cla()
        self.ax_f.cla()

        self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel'])
        self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$')

        self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$')

        if self.chk_log_t.isChecked():
            self.ax_t.plot(
                self.n,
                np.maximum(20 * np.log10(np.abs(self.win)), self.bottom_t))
        else:
            self.ax_t.plot(self.n, self.win)

        if self.chk_half_f.isChecked():
            F = self.F[:len(self.F * self.pad) // 2]
            Win = self.Win[:len(self.F * self.pad) // 2]
        else:
            F = fftshift(self.F)
            Win = fftshift(self.Win)

        if self.chk_log_f.isChecked():
            self.ax_f.plot(
                F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f))
            self.nenbw_disp = 10 * np.log10(self.nenbw)
            self.cgain_disp = 20 * np.log10(self.cgain)
            self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level)
            self.unit_nenbw = "dB"
            self.unit_scale = "dB"
        else:
            self.ax_f.plot(F, Win)
            self.nenbw_disp = self.nenbw
            self.cgain_disp = self.cgain
            self.sidelobe_level_disp = self.sidelobe_level
            self.unit_nenbw = "bins"
            self.unit_scale = ""

        self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked())
        self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked())
        self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked())

        window_name = self.win_dict['name']
        param_txt = ""
        if self.win_dict['n_par'] > 0:
            param_txt = " (" + self.win_dict['par'][0][
                'name_tex'] + " = {0:.3g})".format(
                    self.win_dict['par'][0]['val'])
        if self.win_dict['n_par'] > 1:
            param_txt = param_txt[:-1]\
                + ", {0:s} = {1:.3g})".format(self.win_dict['par'][1]['name_tex'], self.win_dict['par'][1]['val'])

        self.mplwidget.fig.suptitle(r'{0} Window'.format(window_name) +
                                    param_txt)

        # plot a line at the max. sidelobe level
        if self.tbl_sel[3]:
            self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b')

        patch = mpl_patches.Rectangle((0, 0),
                                      1,
                                      1,
                                      fc="white",
                                      ec="white",
                                      lw=0,
                                      alpha=0)
        # Info legend for time domain window
        labels_t = []
        labels_t.append("$N$ = {0:d}".format(self.N))
        self.ax_t.legend([patch],
                         labels_t,
                         loc='best',
                         fontsize='small',
                         fancybox=True,
                         framealpha=0.7,
                         handlelength=0,
                         handletextpad=0)

        # Info legend for frequency domain window
        labels_f = []
        N_patches = 0
        if self.tbl_sel[0]:
            labels_f.append("$NENBW$ = {0:.4g} {1}".format(
                self.nenbw_disp, self.unit_nenbw))
            N_patches += 1
        if self.tbl_sel[1]:
            labels_f.append("$CGAIN$ = {0:.4g} {1}".format(
                self.cgain_disp, self.unit_scale))
            N_patches += 1
        if self.tbl_sel[2]:
            labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f))
            N_patches += 1
        if N_patches > 0:
            self.ax_f.legend([patch] * N_patches,
                             labels_f,
                             loc='best',
                             fontsize='small',
                             fancybox=True,
                             framealpha=0.7,
                             handlelength=0,
                             handletextpad=0)
        np.seterr(**old_settings_seterr)

        self.update_info()
        self.redraw()

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

    def update_info(self):
        """
        Update the text info box for the window
        """
        if 'info' in self.win_dict:
            self.txtInfoBox.setText(self.win_dict['info'])

        self._set_table_item(0, 0, "ENBW", font=self.bfont)  #, sel=True)
        self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp))
        self._set_table_item(0, 2, self.unit_nenbw)
        self._set_table_item(0, 3, "Scale", font=self.bfont)  #, sel=True)
        self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp))
        self._set_table_item(0, 5, self.unit_scale)

        self._set_table_item(1, 0, "1st Zero", font=self.bfont)  #, sel=True)
        self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f))
        self._set_table_item(1, 2, "f_S")

        self._set_table_item(1, 3, "Sidelobes", font=self.bfont)  #, sel=True)
        self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp))
        self._set_table_item(1, 5, self.unit_scale)

        self.tblWinProperties.resizeColumnsToContents()
        self.tblWinProperties.resizeRowsToContents()
#------------------------------------------------------------------------------

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
        self.needs_redraw = False
Exemplo n.º 10
0
class PlotImpz(QWidget):
    """
    Construct a widget for plotting impulse and general transient responses
    """
    def __init__(self, parent):
        super(PlotImpz, self).__init__(parent)

        self.ACTIVE_3D = False
        # initial settings for line edit widgets
        self.stim_freq = 0.02
        self.A = 1.0
        self.bottom = -80
        self._construct_UI()

    def _construct_UI(self):
        self.chkLog = QCheckBox(self)
        self.chkLog.setObjectName("chkLog")
        self.chkLog.setToolTip("<span>Logarithmic scale for y-axis.</span>")
        self.chkLog.setChecked(False)
        self.lblLog = QLabel("Log. y-axis", self)

        self.lblLogBottom = QLabel("Bottom = ", self)
        self.ledLogBottom = QLineEdit(self)
        self.ledLogBottom.setText(str(self.bottom))
        self.ledLogBottom.setToolTip("<span>Minimum display value for log. scale.</span>")
        self.lbldB = QLabel("dB")
        
        self.lblPltStim = QLabel(self)
        self.lblPltStim.setText("Stimulus:")
        self.chkPltStim = QCheckBox("Show", self)
        self.chkPltStim.setChecked(False)
        self.chkPltStim.setToolTip("Show stimulus signal.")
        
        self.lblStimulus = QLabel("Type = ", self)
        self.cmbStimulus = QComboBox(self)
        self.cmbStimulus.addItems(["Pulse","Step","StepErr", "Cos", "Sine", "Rect", "Saw", "RandN", "RandU"])
        self.cmbStimulus.setToolTip("Select stimulus type.")

        self.lblAmp = QLabel("<i>A</i>&nbsp; =", self)
        self.ledAmp = QLineEdit(self)
        self.ledAmp.setText(str(self.A))
        self.ledAmp.setToolTip("Stimulus amplitude.")
        self.ledAmp.setObjectName("stimAmp")

        self.lblFreq = QLabel("<i>f</i>&nbsp; =", self)
        self.ledFreq = QLineEdit(self)
        self.ledFreq.setText(str(self.stim_freq))
        self.ledFreq.setToolTip("Stimulus frequency.")
        self.ledFreq.setObjectName("stimFreq")

        self.lblFreqUnit = QLabel("f_S", self)

        self.lblNPoints = QLabel("<i>N</i>&nbsp; =", self)

        self.ledNPoints = QLineEdit(self)
        self.ledNPoints.setText("0")
        self.ledNPoints.setToolTip("<span>Number of points to calculate and display. "
                                   "N = 0 selects automatically.</span>")

        layHControls = QHBoxLayout()
        
        layHControls.addWidget(self.lblNPoints)
        layHControls.addWidget(self.ledNPoints)
        layHControls.addStretch(2)
        layHControls.addWidget(self.chkLog)
        layHControls.addWidget(self.lblLog)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lblLogBottom)
        layHControls.addWidget(self.ledLogBottom)
        layHControls.addWidget(self.lbldB)
        layHControls.addStretch(2)
        layHControls.addWidget(self.lblPltStim)
        layHControls.addWidget(self.chkPltStim)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lblStimulus)
        layHControls.addWidget(self.cmbStimulus)
        layHControls.addStretch(2)
        layHControls.addWidget(self.lblAmp)
        layHControls.addWidget(self.ledAmp)
        layHControls.addWidget(self.lblFreq)
        layHControls.addWidget(self.ledFreq)
        layHControls.addWidget(self.lblFreqUnit)

        layHControls.addStretch(10)
        
        # This widget encompasses all control subwidgets:
        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layHControls)

        #----------------------------------------------------------------------
        # mplwidget
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.chkLog.clicked.connect(self.draw)
        self.ledNPoints.editingFinished.connect(self.draw)
        self.ledLogBottom.editingFinished.connect(self.draw)
        self.chkPltStim.clicked.connect(self.draw)
        self.cmbStimulus.activated.connect(self.draw)
        self.ledAmp.editingFinished.connect(self.draw)
        self.ledFreq.installEventFilter(self)
        
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)

        self.draw() # initial calculation and drawing

#------------------------------------------------------------------------------
    @pyqtSlot(object)
    def process_signals(self, sig_dict):
        """
        Process signals coming from the navigation toolbar
        """
        if 'update_view' in sig_dict:
            self.update_view()
        elif 'enabled' in sig_dict:
            self.enable_ui(sig_dict['enabled'])
        elif 'home' in sig_dict:
            self.draw()
        else:
            pass

#------------------------------------------------------------------------------
    def enable_ui(self, enabled):
        """
        Triggered when the toolbar is enabled or disabled
        """
        self.frmControls.setEnabled(enabled)
        if enabled:
            # self.init_axes() # called by self.draw
            self.draw()

#------------------------------------------------------------------------------
    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 normalized to f_S with full precision (only if
          `spec_edited`== True) and display the stored value in selected format
        """

        def _store_entry(source):
            if self.spec_edited:
                self.stim_freq = safe_eval(source.text(), self.stim_freq * fb.fil[0]['f_S'],
                                            return_type='float') / fb.fil[0]['f_S']
                self.spec_edited = False # reset flag
                self.draw()
                
        if isinstance(source, QLineEdit): # could be extended for other widgets
            if event.type() == QEvent.FocusIn:
                self.spec_edited = False
                self.load_dict()
            elif event.type() == QEvent.KeyPress:
                self.spec_edited = True # entry has been changed
                key = event.key()
                if key in {Qt.Key_Return, Qt.Key_Enter}:
                    _store_entry(source)
                elif key == Qt.Key_Escape: # revert changes
                    self.spec_edited = False                    
                    source.setText(str(params['FMT'].format(self.stim_freq * fb.fil[0]['f_S'])))
                
            elif event.type() == QEvent.FocusOut:
                _store_entry(source)
                source.setText(str(params['FMT'].format(self.stim_freq * fb.fil[0]['f_S'])))
        # Call base class method to continue normal event processing:
        return super(PlotImpz, self).eventFilter(source, event)

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

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

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

        # recalculate displayed freq spec values for (maybe) changed f_S
        logger.debug("exec load_dict")
        if not self.ledFreq.hasFocus():
            # widget has no focus, round the display
            self.ledFreq.setText(
                str(params['FMT'].format(self.stim_freq * fb.fil[0]['f_S'])))
        else:
            # widget has focus, show full precision
            self.ledFreq.setText(str(self.stim_freq * fb.fil[0]['f_S']))

#------------------------------------------------------------------------------
    def init_axes(self):
        # clear the axes and (re)draw the plot
        #
        try:
            self.mplwidget.fig.delaxes(self.ax_r)
            self.mplwidget.fig.delaxes(self.ax_i)
        except (KeyError, AttributeError, UnboundLocalError):
            pass

        if self.cmplx:
            self.ax_r = self.mplwidget.fig.add_subplot(211)
            self.ax_r.clear()
            self.ax_r.get_xaxis().tick_bottom() # remove axis ticks on top
            self.ax_r.get_yaxis().tick_left() # remove axis ticks right

            self.ax_i = self.mplwidget.fig.add_subplot(212, sharex = self.ax_r)
            self.ax_i.clear()
            self.ax_i.get_xaxis().tick_bottom() # remove axis ticks on top
            self.ax_i.get_yaxis().tick_left() # remove axis ticks right

        else:
            self.ax_r = self.mplwidget.fig.add_subplot(111)
            self.ax_r.clear()
            self.ax_r.get_xaxis().tick_bottom() # remove axis ticks on top
            self.ax_r.get_yaxis().tick_left() # remove axis ticks right


        self.mplwidget.fig.subplots_adjust(hspace = 0.5)  

        if self.ACTIVE_3D: # not implemented / tested yet
            self.ax3d = self.mplwidget.fig.add_subplot(111, projection='3d')

#------------------------------------------------------------------------------
    def update_view(self):
        """
        place holder; should update only the limits without recalculating
        the impulse respons
        """
        self.draw()

#------------------------------------------------------------------------------
    def draw(self):
        if self.mplwidget.mplToolbar.enabled:
            self.draw_impz()

#------------------------------------------------------------------------------
    def draw_impz(self):
        """
        (Re-)calculate h[n] and draw the figure
        """
        log = self.chkLog.isChecked()
        stim = str(self.cmbStimulus.currentText())
        periodic_sig = stim in {"Cos", "Sine","Rect", "Saw"}
        self.lblLogBottom.setVisible(log)
        self.ledLogBottom.setVisible(log)
        self.lbldB.setVisible(log)
        
        self.lblFreq.setVisible(periodic_sig)
        self.ledFreq.setVisible(periodic_sig)
        self.lblFreqUnit.setVisible(periodic_sig)

        self.lblFreqUnit.setText(to_html(fb.fil[0]['freq_specs_unit']))
        self.load_dict()
        
        self.bb = np.asarray(fb.fil[0]['ba'][0])
        self.aa = np.asarray(fb.fil[0]['ba'][1])
        if min(len(self.aa), len(self.bb)) < 2:
            logger.error('No proper filter coefficients: len(a), len(b) < 2 !')
            return

        sos = np.asarray(fb.fil[0]['sos'])
        antiCausal = 'zpkA' in fb.fil[0]
        causal     = not (antiCausal)

        self.f_S  = fb.fil[0]['f_S']
        
        N_entry = safe_eval(self.ledNPoints.text(), 0, return_type='int', sign='pos')
        N = self.calc_n_points(N_entry)
        if N_entry != 0: # automatic calculation
            self.ledNPoints.setText(str(N))

        self.A = safe_eval(self.ledAmp.text(), self.A, return_type='float')
        self.ledAmp.setText(str(self.A))

        t = np.linspace(0, N/self.f_S, N, endpoint=False)

        title_str = r'Impulse Response' # default
        H_str = r'$h[n]$' # default

        # calculate h[n]
        if stim == "Pulse":
            x = np.zeros(N)
            x[0] = self.A # create dirac impulse as input signal
        elif stim == "Step":
            x = self.A * np.ones(N) # create step function
            title_str = r'Step Response'
            H_str = r'$h_{\epsilon}[n]$'
        elif stim == "StepErr":
            x = self.A * np.ones(N) # create step function
            title_str = r'Settling Error'
            H_str = r'$h_{\epsilon, \infty} - h_{\epsilon}[n]$'
            
        elif stim in {"Cos"}:
            x = self.A * np.cos(2 * np.pi * t * float(self.ledFreq.text()))
            if stim == "Cos":
                title_str = r'Transient Response to Cosine Signal'
                H_str = r'$y_{\cos}[n]$'

        elif stim in {"Sine", "Rect"}:
            x = self.A * np.sin(2 * np.pi * t * float(self.ledFreq.text()))
            if stim == "Sine":
                title_str = r'Transient Response to Sine Signal'
                H_str = r'$y_{\sin}[n]$'
            else:
                x = self.A * np.sign(x)
                title_str = r'Transient Response to Rect. Signal'
                H_str = r'$y_{rect}[n]$'

        elif stim == "Saw":
            x = self.A * sig.sawtooth(t * (float(self.ledFreq.text())* 2*np.pi))
            title_str = r'Transient Response to Sawtooth Signal'
            H_str = r'$y_{saw}[n]$'

        elif stim == "RandN":
            x = self.A * np.random.randn(N)
            title_str = r'Transient Response to Gaussian Noise'
            H_str = r'$y_{gauss}[n]$'

        elif stim == "RandU":
            x = self.A * (np.random.rand(N)-0.5)
            title_str = r'Transient Response to Uniform Noise'
            H_str = r'$y_{uni}[n]$'

        else:
            logger.error('Unknown stimulus "{0}"'.format(stim))
            return

        if len(sos) > 0 and (causal): # has second order sections and is causal
            h = sig.sosfilt(sos, x)
        elif (antiCausal):
            h = sig.filtfilt(self.bb, self.aa, x, -1, None)
        else: # no second order sections or antiCausals for current filter
            h = sig.lfilter(self.bb, self.aa, x)

        dc = sig.freqz(self.bb, self.aa, [0])

        if stim == "StepErr":
            h = h - abs(dc[1]) # subtract DC value from response

        h = np.real_if_close(h, tol = 1e3)  # tol specified in multiples of machine eps
        self.cmplx = np.any(np.iscomplex(h))
        if self.cmplx:
            h_i = h.imag
            h = h.real
            H_i_str = r'$\Im\{$' + H_str + '$\}$'
            H_str = r'$\Re\{$' + H_str + '$\}$'
        if log:
            self.bottom = safe_eval(self.ledLogBottom.text(), self.bottom, return_type='float')
            self.ledLogBottom.setText(str(self.bottom))
            H_str = r'$|$ ' + H_str + '$|$ in dB'
            h = np.maximum(20 * np.log10(abs(h)), self.bottom)
            if self.cmplx:
                h_i = np.maximum(20 * np.log10(abs(h_i)), self.bottom)
                H_i_str = r'$\log$ ' + H_i_str + ' in dB'
        else:
            self.bottom = 0

        self.init_axes()

        #================ Main Plotting Routine =========================
        [ml, sl, bl] = self.ax_r.stem(t, h, bottom=self.bottom, markerfmt='o', label = '$h[n]$')
        stem_fmt = params['mpl_stimuli']
        if self.chkPltStim.isChecked():
            [ms, ss, bs] = self.ax_r.stem(t, x, bottom=self.bottom, label = 'Stim.', **stem_fmt)
            ms.set_mfc(stem_fmt['mfc'])
            ms.set_mec(stem_fmt['mec'])
            ms.set_ms(stem_fmt['ms'])
            ms.set_alpha(stem_fmt['alpha'])
            for stem in ss:
                stem.set_linewidth(stem_fmt['lw'])
                stem.set_color(stem_fmt['mec'])
                stem.set_alpha(stem_fmt['alpha'])
            bs.set_visible(False) # invisible bottomline
        expand_lim(self.ax_r, 0.02)
        self.ax_r.set_title(title_str)

        if self.cmplx:
            [ml_i, sl_i, bl_i] = self.ax_i.stem(t, h_i, bottom=self.bottom,
                                                markerfmt='d', label = '$h_i[n]$')
            self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel'])
            # self.ax_r.get_xaxis().set_ticklabels([]) # removes both xticklabels
            # plt.setp(ax_r.get_xticklabels(), visible=False) 
            # is shorter but imports matplotlib, set property directly instead:
            [label.set_visible(False) for label in self.ax_r.get_xticklabels()]
            self.ax_r.set_ylabel(H_str + r'$\rightarrow $')
            self.ax_i.set_ylabel(H_i_str + r'$\rightarrow $')
        else:
            self.ax_r.set_xlabel(fb.fil[0]['plt_tLabel'])
            self.ax_r.set_ylabel(H_str + r'$\rightarrow $')


        if self.ACTIVE_3D: # not implemented / tested yet

            # plotting the stems
            for i in range(len(t)):
              self.ax3d.plot([t[i], t[i]], [h[i], h[i]], [0, h_i[i]],
                             '-', linewidth=2, alpha=.5)

            # plotting a circle on the top of each stem
            self.ax3d.plot(t, h, h_i, 'o', markersize=8,
                           markerfacecolor='none', label='$h[n]$')

            self.ax3d.set_xlabel('x')
            self.ax3d.set_ylabel('y')
            self.ax3d.set_zlabel('z')

        self.redraw()
        
#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()

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

        if N_user == 0: # set number of data points automatically
            if fb.fil[0]['ft'] == 'IIR':
                N = 100
            else:
                N = min(len(self.bb),  100) # FIR: N = number of coefficients (max. 100)
        else:
            N = N_user

        return N
Exemplo n.º 11
0
class Plot_FFT_win(QDialog):
    """
    Create a pop-up widget for displaying time and frequency view of an FFT
    window.

    Data is passed via the dictionary `win_dict` that is specified during
    construction. Available windows, parameters, tooltipps etc are provided
    by the widget `pyfda_fft_windows_lib.QFFTWinSelection`

    Parameters
    ----------

    parent : class instance
        reference to parent

    win_dict : dict
        dictionary derived from `pyfda_fft_windows_lib.all_windows_dict`
        with valid and available windows and their current settings (if applicable)

    sym : bool
        Passed to `get_window()`:
        When True, generate a symmetric window for use in filter design.
        When False (default), generate a periodic window for use in spectral analysis.

    title : str
        Title text for Qt Window

    ignore_close_event : bool
        Disable close event when True (Default)

    Methods
    -------

    - `self.calc_N()`
    - `self.update_view()`:
    - `self.draw()`: calculate window and FFT and draw both
    - `get_win(N)` : Get the window array
    """
    sig_rx = pyqtSignal(object)  # incoming
    sig_tx = pyqtSignal(object)  # outgoing
    from pyfda.libs.pyfda_qt_lib import emit

    def __init__(self,
                 parent,
                 win_dict,
                 sym=False,
                 title='pyFDA Window Viewer',
                 ignore_close_event=True):
        super(Plot_FFT_win, self).__init__(parent)
        # make window stay on top
        qwindow_stay_on_top(self, True)

        self.win_dict = win_dict
        self.sym = sym
        self.ignore_close_event = ignore_close_event
        self.setWindowTitle(title)

        self.needs_calc = True

        self.bottom_f = -80  # min. value for dB display
        self.bottom_t = -60
        # initial number of data points for visualization
        self.N_view = 32

        self.pad = 16  # zero padding factor for smooth FFT plot

        # initial settings for checkboxes
        self.tbl_sel = [True, True, False, False]
        #    False, False, False, False]
        self.tbl_cols = 6
        self.tbl_rows = len(self.tbl_sel) // (self.tbl_cols // 3)

        self._construct_UI()
        self.calc_win_draw()

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

    def closeEvent(self, event):
        """
        Catch `closeEvent` (user has tried to close the FFT window) and send a
        signal to parent to decide how to proceed.

        This can be disabled by setting `self.ignore_close_event = False` e.g.
        for instantiating the widget as a standalone window.
        """
        if self.ignore_close_event:
            event.ignore()
            self.emit({'closeEvent': ''})

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

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx:

        - `self.calc_N`
        - `self.update_view`:
        - `self.draw`: calculate window and FFT and draw both
        """
        # logger.debug("PROCESS_SIG_RX - vis={0}, needs_calc={1}\n{2}"
        #              .format(self.isVisible(), self.needs_calc, pprint_log(dict_sig)))

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

        elif not self.isVisible():
            self.needs_calc = True

        elif 'view_changed' in dict_sig and 'fft_win' in dict_sig['view_changed']\
                or self.needs_calc:

            self.calc_win_draw()
            self.needs_calc = False

        elif 'home' in dict_sig:
            self.update_view()

        else:
            logger.error("Unknown content of dict_sig: {0}".format(dict_sig))

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

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.bfont = QFont()
        self.bfont.setBold(True)

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

        self.lbl_N = QLabel(to_html("N =", frmt='bi'))
        self.led_N = QLineEdit(self)
        self.led_N.setText(str(self.N_view))
        self.led_N.setMaximumWidth(qtext_width(N_x=8))
        self.led_N.setToolTip(
            "<span>Number of window data points to display.</span>")

        # By default, the enter key triggers the default 'dialog action' in QDialog
        # widgets. This activates one of the pushbuttons.
        self.but_log_t = QPushButton("dB", default=False, autoDefault=False)
        self.but_log_t.setMaximumWidth(qtext_width(" dB "))
        self.but_log_t.setObjectName("chk_log_time")
        self.but_log_t.setCheckable(True)
        self.but_log_t.setChecked(False)
        self.but_log_t.setToolTip("Display in dB")

        self.led_log_bottom_t = QLineEdit(self)
        self.led_log_bottom_t.setVisible(self.but_log_t.isChecked())
        self.led_log_bottom_t.setText(str(self.bottom_t))
        self.led_log_bottom_t.setMaximumWidth(qtext_width(N_x=6))
        self.led_log_bottom_t.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        self.lbl_log_bottom_t = QLabel(to_html("min =", frmt='bi'), self)
        self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked())

        self.but_norm_f = QPushButton("Max=1",
                                      default=False,
                                      autoDefault=False)
        self.but_norm_f.setCheckable(True)
        self.but_norm_f.setChecked(True)
        self.but_norm_f.setMaximumWidth(qtext_width(text=" Max=1 "))
        self.but_norm_f.setToolTip(
            "Normalize window spectrum for a maximum of 1.")

        self.but_half_f = QPushButton("0...½",
                                      default=False,
                                      autoDefault=False)
        self.but_half_f.setCheckable(True)
        self.but_half_f.setChecked(True)
        self.but_half_f.setMaximumWidth(qtext_width(text=" 0...½ "))
        self.but_half_f.setToolTip(
            "Display window spectrum in the range 0 ... 0.5 f_S.")

        # By default, the enter key triggers the default 'dialog action' in QDialog
        # widgets. This activates one of the pushbuttons.
        self.but_log_f = QPushButton("dB", default=False, autoDefault=False)
        self.but_log_f.setMaximumWidth(qtext_width(" dB "))
        self.but_log_f.setObjectName("chk_log_freq")
        self.but_log_f.setToolTip("<span>Display in dB.</span>")
        self.but_log_f.setCheckable(True)
        self.but_log_f.setChecked(True)

        self.lbl_log_bottom_f = QLabel(to_html("min =", frmt='bi'), self)
        self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked())

        self.led_log_bottom_f = QLineEdit(self)
        self.led_log_bottom_f.setVisible(self.but_log_t.isChecked())
        self.led_log_bottom_f.setText(str(self.bottom_f))
        self.led_log_bottom_f.setMaximumWidth(qtext_width(N_x=6))
        self.led_log_bottom_f.setToolTip(
            "<span>Minimum display value for log. scale.</span>")

        # ----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        # ----------------------------------------------------------------------
        layH_win_select = QHBoxLayout()
        layH_win_select.addWidget(self.qfft_win_select)
        layH_win_select.setContentsMargins(0, 0, 0, 0)
        layH_win_select.addStretch(1)
        frmQFFT = QFrame(self)
        frmQFFT.setObjectName("frmQFFT")
        frmQFFT.setLayout(layH_win_select)

        hline = QHLine()

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.lbl_N)
        layHControls.addWidget(self.led_N)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lbl_log_bottom_t)
        layHControls.addWidget(self.led_log_bottom_t)
        layHControls.addWidget(self.but_log_t)
        layHControls.addStretch(5)
        layHControls.addWidget(QVLine(width=2))
        layHControls.addStretch(5)
        layHControls.addWidget(self.but_norm_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.but_half_f)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lbl_log_bottom_f)
        layHControls.addWidget(self.led_log_bottom_f)
        layHControls.addWidget(self.but_log_f)

        layVControls = QVBoxLayout()
        layVControls.addWidget(frmQFFT)
        layVControls.addWidget(hline)
        layVControls.addLayout(layHControls)

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

        # ----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # Layout layVMainMpl (VBox) is defined within MplWidget, additional
        # widgets can be added below the matplotlib widget (here: frmControls)
        #
        # ----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(0, 0, 0, 0)

        # ----------------------------------------------------------------------
        #               ### frmInfo ###
        #
        # This widget encompasses the text info box and the table with window
        # parameters.
        # ----------------------------------------------------------------------
        self.tbl_win_props = QTableWidget(self.tbl_rows, self.tbl_cols, self)
        self.tbl_win_props.setAlternatingRowColors(True)
        # Auto-resize of table can be set using the header (although it is invisible)
        self.tbl_win_props.verticalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        # Only the columns with data are stretched, the others are minimum size
        self.tbl_win_props.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.Stretch)
        self.tbl_win_props.horizontalHeader().setSectionResizeMode(
            4, QHeaderView.Stretch)
        self.tbl_win_props.verticalHeader().setVisible(False)
        self.tbl_win_props.horizontalHeader().setVisible(False)
        self.tbl_win_props.setSizePolicy(QSizePolicy.MinimumExpanding,
                                         QSizePolicy.MinimumExpanding)
        self.tbl_win_props.setFixedHeight(
            self.tbl_win_props.rowHeight(0) * self.tbl_rows +
            self.tbl_win_props.frameWidth() * 2)
        # self.tbl_win_props.setVerticalScrollBarPolicy(
        #     Qt.ScrollBarAlwaysOff)
        # self.tbl_win_props.setHorizontalScrollBarPolicy(
        #     Qt.ScrollBarAlwaysOff)

        self._construct_table(self.tbl_rows, self.tbl_cols, " ")

        self.txtInfoBox = QTextBrowser(self)

        layVInfo = QVBoxLayout(self)
        layVInfo.addWidget(self.tbl_win_props)
        layVInfo.addWidget(self.txtInfoBox)

        frmInfo = QFrame(self)
        frmInfo.setObjectName("frmInfo")
        frmInfo.setLayout(layVInfo)

        # ----------------------------------------------------------------------
        #               ### splitter ###
        #
        # This widget encompasses all subwidgets
        # ----------------------------------------------------------------------

        splitter = QSplitter(self)
        splitter.setOrientation(Qt.Vertical)
        splitter.addWidget(self.mplwidget)
        splitter.addWidget(frmInfo)

        # setSizes uses absolute pixel values, but can be "misused" by
        # specifying values that are way too large: in this case, the space
        # is distributed according to the _ratio_ of the values:
        splitter.setSizes([3000, 800])

        layVMain = QVBoxLayout()
        layVMain.addWidget(splitter)
        self.setLayout(layVMain)

        # ----------------------------------------------------------------------
        #           Set subplots
        #
        self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2)
        self.ax_t = self.ax[0]
        self.ax_f = self.ax[1]

        self.calc_win_draw()  # initial calculation and drawing

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        self.sig_rx.connect(self.qfft_win_select.sig_rx)

        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.but_log_f.clicked.connect(self.update_view)
        self.but_log_t.clicked.connect(self.update_view)
        self.led_log_bottom_t.editingFinished.connect(self.update_bottom)
        self.led_log_bottom_f.editingFinished.connect(self.update_bottom)

        self.led_N.editingFinished.connect(self.calc_win_draw)

        self.but_norm_f.clicked.connect(self.calc_win_draw)
        self.but_half_f.clicked.connect(self.update_view)

        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.tbl_win_props.itemClicked.connect(self._handle_item_clicked)

        self.qfft_win_select.sig_tx.connect(self.update_fft_win)

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

    def _construct_table(self, rows, cols, val):
        """
        Create a table with `rows` and `cols`, organized in sets of 3:
        Name (with a checkbox) - value - unit
        each item.

        Parameters
        ----------

        rows : int
            number of rows

        cols : int
            number of columns (must be multiple of 3)

        val : str
            initialization value for the table

        Returns
        -------
        None
        """
        for r in range(rows):
            for c in range(cols):
                item = QTableWidgetItem(val)
                if c % 3 == 0:
                    item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                    if self.tbl_sel[r * 2 + c % 3]:
                        item.setCheckState(Qt.Checked)
                    else:
                        item.setCheckState(Qt.Unchecked)
                self.tbl_win_props.setItem(r, c, item)

    # https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget

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

    def update_fft_win(self, dict_sig=None):
        """
        Update FFT window when window or parameters have changed and
        pass thru 'view_changed':'fft_win_type' or 'fft_win_par'
        """
        self.calc_win_draw()
        self.emit(dict_sig)

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

    def calc_win_draw(self):
        """
        (Re-)Calculate the window, its FFT and some characteristic values and update
        the plot of the window and its FFT. This should be triggered when the
        window type or length or a parameters has been changed.

        Returns
        -------
        None

        Attributes
        ----------

        """
        self.N_view = safe_eval(self.led_N.text(),
                                self.N_view,
                                sign='pos',
                                return_type='int')
        self.led_N.setText(str(self.N_view))
        self.n = np.arange(self.N_view)
        self.win_view = self.qfft_win_select.get_window(self.N_view,
                                                        sym=self.sym)

        if self.qfft_win_select.err:
            self.qfft_win_select.dict2ui()

        self.nenbw = self.N_view * np.sum(np.square(self.win_view))\
            / np.square(np.sum(self.win_view))
        self.cgain = np.sum(self.win_view) / self.N_view  # coherent gain

        # calculate the FFT of the window with a zero padding factor
        # of `self.pad`
        self.F = fftfreq(self.N_view * self.pad, d=1. / fb.fil[0]['f_S'])
        self.Win = np.abs(fft(self.win_view, self.N_view * self.pad))

        # Correct gain for periodic signals (coherent gain)
        if self.but_norm_f.isChecked():
            self.Win /= (self.N_view * self.cgain)

        # calculate frequency of first zero and maximum sidelobe level
        first_zero = argrelextrema(self.Win[:(self.N_view * self.pad) // 2],
                                   np.less)
        if np.shape(first_zero)[1] > 0:
            first_zero = first_zero[0][0]
            self.first_zero_f = self.F[first_zero]
            self.sidelobe_level = np.max(
                self.Win[first_zero:(self.N_view * self.pad) // 2])
        else:
            self.first_zero_f = np.nan
            self.sidelobe_level = 0

        self.update_view()

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

    def _set_table_item(self, row, col, val, font=None, sel=None):
        """
        Set the table item with the index `row, col` and the value val
        """
        item = self.tbl_win_props.item(row, col)
        item.setText(str(val))

        if font:
            self.tbl_win_props.item(row, col).setFont(font)

        if sel is True:
            item.setCheckState(Qt.Checked)
        if sel is False:
            item.setCheckState(Qt.Unchecked)
        # when sel is not specified, don't change anything

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

    def _handle_item_clicked(self, item):
        if item.column() % 3 == 0:  # clicked on checkbox
            num = item.row() * 2 + item.column() // 3
            if item.checkState() == Qt.Checked:
                self.tbl_sel[num] = True
                logger.debug('"{0}:{1}" Checked'.format(item.text(), num))
            else:
                self.tbl_sel[num] = False
                logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num))

        elif item.column() % 3 == 1:  # clicked on value field
            logger.info("{0:s} copied to clipboard.".format(item.text()))
            fb.clipboard.setText(item.text())

        self.update_view()

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

    def update_bottom(self):
        """
        Update log bottom settings
        """
        self.bottom_t = safe_eval(self.led_log_bottom_t.text(),
                                  self.bottom_t,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_t.setText(str(self.bottom_t))

        self.bottom_f = safe_eval(self.led_log_bottom_f.text(),
                                  self.bottom_f,
                                  sign='neg',
                                  return_type='float')
        self.led_log_bottom_f.setText(str(self.bottom_f))

        self.update_view()

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

    def update_view(self):
        """
        Draw the figure with new limits, scale, lin/log  etc without
        recalculating the window or its FFT.
        """
        # suppress "divide by zero in log10" warnings
        old_settings_seterr = np.seterr()
        np.seterr(divide='ignore')

        self.ax_t.cla()
        self.ax_f.cla()

        self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel'])
        self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$')

        self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$')

        if self.but_log_t.isChecked():
            self.ax_t.plot(
                self.n,
                np.maximum(20 * np.log10(np.abs(self.win_view)),
                           self.bottom_t))
        else:
            self.ax_t.plot(self.n, self.win_view)

        if self.but_half_f.isChecked():
            F = self.F[:len(self.F * self.pad) // 2]
            Win = self.Win[:len(self.F * self.pad) // 2]
        else:
            F = fftshift(self.F)
            Win = fftshift(self.Win)

        if self.but_log_f.isChecked():
            self.ax_f.plot(
                F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f))
            self.nenbw_disp = 10 * np.log10(self.nenbw)
            self.cgain_disp = 20 * np.log10(self.cgain)
            self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level)
            self.nenbw_unit = "dB"
            self.cgain_unit = "dB"
        else:
            self.ax_f.plot(F, Win)
            self.nenbw_disp = self.nenbw
            self.cgain_disp = self.cgain
            self.sidelobe_level_disp = self.sidelobe_level
            self.nenbw_unit = "bins"
            self.cgain_unit = ""

        self.led_log_bottom_t.setVisible(self.but_log_t.isChecked())
        self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked())
        self.led_log_bottom_f.setVisible(self.but_log_f.isChecked())
        self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked())

        cur = self.win_dict['cur_win_name']
        cur_win_d = self.win_dict[cur]
        param_txt = ""
        if cur_win_d['n_par'] > 0:
            if type(cur_win_d['par'][0]['val']) in {str}:
                p1 = cur_win_d['par'][0]['val']
            else:
                p1 = "{0:.3g}".format(cur_win_d['par'][0]['val'])
            param_txt = " ({0:s} = {1:s})".format(
                cur_win_d['par'][0]['name_tex'], p1)

        if self.win_dict[cur]['n_par'] > 1:
            if type(cur_win_d['par'][1]['val']) in {str}:
                p2 = cur_win_d['par'][1]['val']
            else:
                p2 = "{0:.3g}".format(cur_win_d['par'][1]['val'])
            param_txt = param_txt[:-1]\
                + ", {0:s} = {1:s})".format(cur_win_d['par'][1]['name_tex'], p2)

        self.mplwidget.fig.suptitle(r'{0} Window'.format(cur) + param_txt)

        # plot a line at the max. sidelobe level
        if self.tbl_sel[3]:
            self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b')

        patch = mpl_patches.Rectangle((0, 0),
                                      1,
                                      1,
                                      fc="white",
                                      ec="white",
                                      lw=0,
                                      alpha=0)
        # Info legend for time domain window
        labels_t = []
        labels_t.append("$N$ = {0:d}".format(self.N_view))
        self.ax_t.legend([patch],
                         labels_t,
                         loc='best',
                         fontsize='small',
                         fancybox=True,
                         framealpha=0.7,
                         handlelength=0,
                         handletextpad=0)

        # Info legend for frequency domain window
        labels_f = []
        N_patches = 0
        if self.tbl_sel[0]:
            labels_f.append("$NENBW$ = {0:.4g} {1}".format(
                self.nenbw_disp, self.nenbw_unit))
            N_patches += 1
        if self.tbl_sel[1]:
            labels_f.append("$CGAIN$ = {0:.4g} {1}".format(
                self.cgain_disp, self.cgain_unit))
            N_patches += 1
        if self.tbl_sel[2]:
            labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f))
            N_patches += 1

        if N_patches > 0:
            self.ax_f.legend([patch] * N_patches,
                             labels_f,
                             loc='best',
                             fontsize='small',
                             fancybox=True,
                             framealpha=0.7,
                             handlelength=0,
                             handletextpad=0)
        np.seterr(**old_settings_seterr)

        self.update_info()
        self.redraw()

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

    def update_info(self):
        """
        Update the text info box for the window
        """
        cur = self.win_dict['cur_win_name']
        if 'info' in self.win_dict[cur]:
            self.txtInfoBox.setText(self.win_dict[cur]['info'])
        else:
            self.txtInfoBox.clear()

        self._set_table_item(0, 0, "NENBW", font=self.bfont)  # , sel=True)
        self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp))
        self._set_table_item(0, 2, self.nenbw_unit)
        self._set_table_item(0, 3, "Scale", font=self.bfont)  # , sel=True)
        self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp))
        self._set_table_item(0, 5, self.cgain_unit)

        self._set_table_item(1, 0, "1st Zero", font=self.bfont)  # , sel=True)
        self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f))
        self._set_table_item(1, 2, "f_S")

        self._set_table_item(1, 3, "Sidelobes", font=self.bfont)  # , sel=True)
        self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp))
        self._set_table_item(1, 5, self.cgain_unit)

        self.tbl_win_props.resizeColumnsToContents()
        self.tbl_win_props.resizeRowsToContents()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 12
0
class PlotTauG(QWidget):
    """
    Widget for plotting the group delay
    """

    def __init__(self, parent):
        super(PlotTauG, self).__init__(parent)
        self.verbose = False # suppress warnings

# =============================================================================
# #### not needed at the moment ###
#         self.chkWarnings = QCheckBox("Enable Warnings", self)
#         self.chkWarnings.setChecked(False)
#         self.chkWarnings.setToolTip("Print warnings about singular group delay")
# 
#         layHControls = QHBoxLayout()
#         layHControls.addStretch(10)
#         layHControls.addWidget(self.chkWarnings)
#         
#         # This widget encompasses all control subwidgets:
#         self.frmControls = QFrame(self)
#         self.frmControls.setObjectName("frmControls")
#         self.frmControls.setLayout(layHControls)
# 
# =============================================================================
        self.mplwidget = MplWidget(self)
#        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()        
        self.draw() # initial drawing of tau_g

        #----------------------------------------------------------------------
        # SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)

#------------------------------------------------------------------------------
    @pyqtSlot(object)
    def process_signals(self, sig_dict):
        """
        Process signals coming from the navigation toolbar
        """
        if 'update_view' in sig_dict:
            self.update_view()
        elif 'enabled' in sig_dict:
            self.enable_ui(sig_dict['enabled'])
        elif 'home' in sig_dict:
            self.draw()
        else:
            pass

#------------------------------------------------------------------------------
    def enable_ui(self, enabled):
        """
        Triggered when the toolbar is enabled or disabled
        """
#        self.frmControls.setEnabled(enabled)
        if enabled:
            self.init_axes()
            self.draw()

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes
        """
        self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.clear()
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    def calc_tau_g(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        bb = fb.fil[0]['ba'][0]
        aa = fb.fil[0]['ba'][1]

        # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.tau_g = grpdelay(bb, aa, params['N_FFT'], whole = True,
            verbose = self.verbose) # self.chkWarnings.isChecked())

        # Zero phase filters have no group delay (Causal+AntiCausal)
        if 'baA' in fb.fil[0]:
           self.tau_g = np.zeros(self.tau_g.size)


#------------------------------------------------------------------------------
    def draw(self):
        if self.mplwidget.mplToolbar.enabled:
            self.calc_tau_g()
            self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        #========= select frequency range to be displayed =====================
        #=== shift, scale and select: W -> F, H_cplx -> H_c
        f_S2 = fb.fil[0]['f_S'] / 2.
        F = self.W * f_S2 / np.pi

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

        #================ Main Plotting Routine =========================
        #===  clear the axes and (re)draw the plot
            
        if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $'
        else:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\
                + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $'
            tau_g = tau_g / fb.fil[0]['f_S']

        #---------------------------------------------------------
        self.ax.clear() # need to clear, doesn't overwrite
        line_tau_g, = self.ax.plot(F, tau_g, label = "Group Delay")
        #---------------------------------------------------------

        self.ax.set_title(r'Group Delay $ \tau_g$')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(tau_str)
        # widen y-limits to suppress numerical inaccuracies when tau_g = constant
        self.ax.set_ylim([max(min(tau_g)-0.5,0), max(tau_g) + 0.5])
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 13
0
class Plot_Phi(QWidget):
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self, parent):
        super(Plot_Phi, self).__init__(parent)
        self.data_changed = True
        self.view_change= True
        self.size_changed = True
        self.tool_tip = "Phase frequency response"
        self.tab_label = "phi(f)"
        self._construct_UI()

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

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """

        self.cmbUnitsPhi = QComboBox(self)
        units = ["rad", "rad/pi",  "deg"]
        scales = [1.,   1./ np.pi, 180./np.pi]
        for unit, scale in zip(units, scales):
            self.cmbUnitsPhi.addItem(unit, scale)
        self.cmbUnitsPhi.setObjectName("cmbUnitsA")
        self.cmbUnitsPhi.setToolTip("Set unit for phase.")
        self.cmbUnitsPhi.setCurrentIndex(0)
        self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkWrap = QCheckBox("Wrapped Phase", self)
        self.chkWrap.setChecked(False)
        self.chkWrap.setToolTip("Plot phase wrapped to +/- pi")

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.cmbUnitsPhi)
        layHControls.addWidget(self.chkWrap)
        layHControls.addStretch(10)

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

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self.draw() # initial drawing

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.chkWrap.clicked.connect(self.draw)
        self.cmbUnitsPhi.currentIndexChanged.connect(self.draw)
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes - this is only called once
        """
        if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes
            self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    def calc_resp(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        # calculate H_cplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], wholeF=True)
        # replace nan and inf by finite values, otherwise np.unwrap yields
        # an array full of nans
        self.H_cmplx = np.nan_to_num(self.H_cmplx)

#------------------------------------------------------------------------------
    def draw(self):
        """
        Main entry point:
        Re-calculate \|H(f)\| and draw the figure
        """
        self.calc_resp()
        self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """

        self.unitPhi = qget_cmb_box(self.cmbUnitsPhi, data=False)

        f_S2 = fb.fil[0]['f_S'] / 2.

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

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

        y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in '
        if self.unitPhi == 'rad':
            y_str += 'rad ' + r'$\rightarrow $'
            scale = 1.
        elif self.unitPhi == 'rad/pi':
            y_str += 'rad' + r'$ / \pi \;\rightarrow $'
            scale = 1./ np.pi
        else:
            y_str += 'deg ' + r'$\rightarrow $'
            scale = 180./np.pi
        fb.fil[0]['plt_phiLabel'] = y_str
        fb.fil[0]['plt_phiUnit'] = self.unitPhi

        if self.chkWrap.isChecked():
            phi_plt = np.angle(H) * scale
        else:
            phi_plt = np.unwrap(np.angle(H)) * scale

        #---------------------------------------------------------
        self.ax.clear() # need to clear, doesn't overwrite
        line_phi, = self.ax.plot(F, phi_plt)
        #---------------------------------------------------------

        self.ax.set_title(r'Phase Frequency Response')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(y_str)
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 14
0
class Plot_PZ(QWidget):
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self):
        super().__init__()
        self.needs_calc = True   # flag whether filter data has been changed
        self.needs_draw = False  # flag whether whether figure needs to be drawn
                                 # with new limits etc. (not implemented yet)
        self.tool_tip = "Pole / zero plan"
        self.tab_label = "P / Z"

        self.cmb_overlay_items = [
            "<span>Add various overlays to P/Z diagram.</span>",
            ("none", "None", ""),
            ("h(f)", "|H(f)|",
             "<span>Show |H(f)| wrapped around the unit circle between 0 resp. -120 dB "
             "and max(H(f)).</span>"),
            ("contour", "Contour", "<span>Show contour lines for |H(z)|</span>"),
            ("contourf", "Contourf", "<span>Show filled contours for |H(z)|</span>"),
            ]
        self.cmb_overlay_default = "none"  # default setting
        self.cmap = "viridis"  # colormap

        self.zmin = 0
        self.zmax = 2
        self.zmin_dB = -80
        self.zmax_dB = np.round(20 * np.log10(self.zmax), 2)
        self._construct_UI()

# ------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig: dict = None) -> None:
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        # logger.info("Processing {0} | needs_draw = {1}, visible = {2}"\
        #              .format(dict_sig, self.needs_calc, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc:
                self.draw()
                self.needs_calc = False
                self.needs_draw = False
            elif 'view_changed' in dict_sig or self.needs_draw:
                self.update_view()
                self.needs_draw = False
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.draw()
        else:
            if 'data_changed' in dict_sig:
                self.needs_calc = True
            elif 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.needs_draw = True

# ------------------------------------------------------------------------------
    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """
        self.lbl_overlay = QLabel(to_html("Overlay:", frmt='bi'), self)
        self.cmb_overlay = QComboBox(self)
        qcmb_box_populate(
            self.cmb_overlay, self.cmb_overlay_items, self.cmb_overlay_default)

        self.but_log = PushButton(" Log.", checked=True)
        self.but_log.setObjectName("but_log")
        self.but_log.setToolTip("<span>Log. scale for overlays.</span>")

        self.diaRad_Hf = QDial(self)
        self.diaRad_Hf.setRange(2, 10)
        self.diaRad_Hf.setValue(2)
        self.diaRad_Hf.setTracking(False)  # produce less events when turning
        self.diaRad_Hf.setFixedHeight(30)
        self.diaRad_Hf.setFixedWidth(30)
        self.diaRad_Hf.setWrapping(False)
        self.diaRad_Hf.setToolTip("<span>Set max. radius for |H(f)| plot.</span>")

        self.lblRad_Hf = QLabel("Radius", self)

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

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

        self.but_fir_poles = PushButton(" FIR Poles ", checked=True)
        self.but_fir_poles.setToolTip("<span>Show FIR poles at the origin.</span>")

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.lbl_overlay)
        layHControls.addWidget(self.cmb_overlay)
        layHControls.addWidget(self.but_log)
        layHControls.addWidget(self.diaRad_Hf)
        layHControls.addWidget(self.lblRad_Hf)
        layHControls.addWidget(self.lblTop)
        layHControls.addWidget(self.ledTop)
        layHControls.addWidget(self.lblTopdB)
        layHControls.addWidget(self.lblBottom)
        layHControls.addWidget(self.ledBottom)
        layHControls.addWidget(self.lblBottomdB)
        layHControls.addStretch(10)
        layHControls.addWidget(self.but_fir_poles)

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

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

        self.init_axes()

        self._log_clicked()  # calculate and draw poles and zeros

        # ----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)
        # ----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
        self.cmb_overlay.currentIndexChanged.connect(self.draw)
        self.but_log.clicked.connect(self._log_clicked)
        self.ledBottom.editingFinished.connect(self._log_clicked)
        self.ledTop.editingFinished.connect(self._log_clicked)
        self.diaRad_Hf.valueChanged.connect(self.draw)
        self.but_fir_poles.clicked.connect(self.draw)

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

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

        self.draw()

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

    # --------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etcs without recalculating H(f)
        -- not yet implemented, just use draw() for the moment
        """
        self.draw()

    # --------------------------------------------------------------------------
    def draw(self):
        self.but_fir_poles.setVisible(fb.fil[0]['ft'] == 'FIR')
        contour = qget_cmb_box(self.cmb_overlay) in {"contour", "contourf"}
        self.ledBottom.setVisible(contour)
        self.lblBottom.setVisible(contour)
        self.lblBottomdB.setVisible(contour and self.but_log.isChecked())
        self.ledTop.setVisible(contour)
        self.lblTop.setVisible(contour)
        self.lblTopdB.setVisible(contour and self.but_log.isChecked())

        if True:
            self.init_axes()
        self.draw_pz()

    # --------------------------------------------------------------------------
    def draw_pz(self):
        """
        (re)draw P/Z plot
        """
        p_marker = params['P_Marker']
        z_marker = params['Z_Marker']

        zpk = fb.fil[0]['zpk']

        # add antiCausals if they exist (must take reciprocal to plot)
        if 'rpk' in fb.fil[0]:
            zA = fb.fil[0]['zpk'][0]
            zA = np.conj(1./zA)
            pA = fb.fil[0]['zpk'][1]
            pA = np.conj(1./pA)
            zC = np.append(zpk[0], zA)
            pC = np.append(zpk[1], pA)
            zpk[0] = zC
            zpk[1] = pC

        self.ax.clear()

        [z, p, k] = self.zplane(
            z=zpk[0], p=zpk[1], k=zpk[2], plt_ax=self.ax,
            plt_poles=self.but_fir_poles.isChecked() or fb.fil[0]['ft'] == 'IIR',
            mps=p_marker[0], mpc=p_marker[1], mzs=z_marker[0], mzc=z_marker[1])

        self.ax.xaxis.set_minor_locator(AutoMinorLocator())  # enable minor ticks
        self.ax.yaxis.set_minor_locator(AutoMinorLocator())  # enable minor ticks
        self.ax.set_title(r'Pole / Zero Plot')
        self.ax.set_xlabel('Real axis')
        self.ax.set_ylabel('Imaginary axis')

        overlay = qget_cmb_box(self.cmb_overlay)
        self.but_log.setVisible(overlay != "none")

        self.draw_Hf(r=self.diaRad_Hf.value(), Hf_visible=(overlay == "h(f)"))

        self.draw_contours(overlay)

        self.redraw()

    # --------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()

    # --------------------------------------------------------------------------
    def zplane(self, b=None, a=1, z=None, p=None, k=1,  pn_eps=1e-3, analog=False,
               plt_ax=None, plt_poles=True, style='equal', anaCircleRad=0, lw=2,
               mps=10, mzs=10, mpc='r', mzc='b', plabel='', zlabel=''):
        """
        Plot the poles and zeros in the complex z-plane either from the
        coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False)
        or directly from the zeros and poles (z,p) (zpk = True).

        When only b is given, an FIR filter with all poles at the origin is assumed.

        Parameters
        ----------
        b :  array_like
             Numerator coefficients (transversal part of filter)
             When b is not None, poles and zeros are determined from the coefficients
             b and a

        a :  array_like (optional, default = 1 for FIR-filter)
             Denominator coefficients (recursive part of filter)

        z :  array_like, default = None
             Zeros
             When b is None, poles and zeros are taken directly from z and p

        p :  array_like, default = None
             Poles

        analog : boolean (default: False)
            When True, create a P/Z plot suitable for the s-plane, i.e. suppress
            the unit circle (unless anaCircleRad > 0) and scale the plot for
            a good display of all poles and zeros.

        pn_eps : float (default : 1e-2)
             Tolerance for separating close poles or zeros

        plt_ax : handle to axes for plotting (default: None)
            When no axes is specified, the current axes is determined via plt.gca()

        plt_poles : Boolean (default : True)
            Plot poles. This can be used to suppress poles for FIR systems
            where all poles are at the origin.

        style : string (default: 'scaled')
            Style of the plot, for `style == 'scaled'` make scale of x- and y-
            axis equal, `style == 'equal'` forces x- and y-axes to be equal. This
            is passed as an argument to the matplotlib `ax.axis(style)`

        mps : integer  (default: 10)
            Size for pole marker

        mzs : integer (default: 10)
            Size for zero marker

        mpc : char (default: 'r')
            Pole marker colour

        mzc : char (default: 'b')
            Zero marker colour

        lw : integer (default:  2)
            Linewidth for unit circle

        plabel, zlabel : string (default: '')
            This string is passed to the plot command for poles and zeros and
            can be displayed by legend()


        Returns
        -------
        z, p, k : ndarray


        Notes
        -----
        """
        # TODO:
        # - polar option
        # - add keywords for color of circle -> **kwargs
        # - add option for multi-dimensional arrays and zpk data

        # make sure that all inputs are (at least 1D) arrays
        b = np.atleast_1d(b)
        a = np.atleast_1d(a)
        z = np.atleast_1d(z)
        p = np.atleast_1d(p)

        if b.any():  # coefficients were specified
            if len(b) < 2 and len(a) < 2:
                logger.error('No proper filter coefficients: both b and a are scalars!')
                return z, p, k

            # The coefficients are less than 1, normalize the coefficients
            if np.max(b) > 1:
                kn = np.max(b)
                b = b / float(kn)
            else:
                kn = 1.

            if np.max(a) > 1:
                kd = np.max(a)
                a = a / abs(kd)
            else:
                kd = 1.

            # Calculate the poles, zeros and scaling factor
            p = np.roots(a)
            z = np.roots(b)
            k = kn/kd
        elif not (len(p) or len(z)):  # P/Z were specified
            logger.error('Either b,a or z,p must be specified!')
            return z, p, k

        # find multiple poles and zeros and their multiplicities
        if len(p) < 2:  # single pole, [None] or [0]
            if not p or p == 0:  # only zeros, create equal number of poles at origin
                p = np.array(0, ndmin=1)
                num_p = np.atleast_1d(len(z))
            else:
                num_p = [1.]  # single pole != 0
        else:
            # p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg')
            p, num_p = unique_roots(p, tol=pn_eps, rtype='avg')
    #        p = np.array(p); num_p = np.ones(len(p))
        if len(z) > 0:
            z, num_z = unique_roots(z, tol=pn_eps, rtype='avg')
    #        z = np.array(z); num_z = np.ones(len(z))
            # z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg')
        else:
            num_z = []

        if analog is False:
            # create the unit circle for the z-plane
            uc = patches.Circle((0, 0), radius=1, fill=False,
                                color='grey', ls='solid', zorder=1)
            plt_ax.add_patch(uc)
            plt_ax.axis(style)
        #    ax.spines['left'].set_position('center')
        #    ax.spines['bottom'].set_position('center')
        #    ax.spines['right'].set_visible(True)
        #    ax.spines['top'].set_visible(True)

        else:  # s-plane
            if anaCircleRad > 0:
                # plot a circle with radius = anaCircleRad
                uc = patches.Circle((0, 0), radius=anaCircleRad, fill=False,
                                    color='grey', ls='solid', zorder=1)
                plt_ax.add_patch(uc)
            # plot real and imaginary axis
            plt_ax.axhline(lw=2, color='k', zorder=1)
            plt_ax.axvline(lw=2, color='k', zorder=1)

        # Plot the zeros
        plt_ax.scatter(z.real, z.imag, s=mzs*mzs, zorder=2, marker='o',
                       facecolor='none', edgecolor=mzc, lw=lw, label=zlabel)
        # and print their multiplicity
        for i in range(len(z)):
            logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i]))
            if num_z[i] > 1:
                plt_ax.text(np.real(z[i]), np.imag(z[i]), '  (' + str(num_z[i]) + ')',
                            va='top', color=mzc)
        if plt_poles:
            # Plot the poles
            plt_ax.scatter(p.real, p.imag, s=mps*mps, zorder=2, marker='x',
                           color=mpc, lw=lw, label=plabel)
            # and print their multiplicity
            for i in range(len(p)):
                logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i]))
                if num_p[i] > 1:
                    plt_ax.text(np.real(p[i]), np.imag(p[i]), '  (' + str(num_p[i]) + ')',
                                va='bottom', color=mpc)

# =============================================================================
#            # increase distance between ticks and labels
#            # to give some room for poles and zeros
#         for tick in ax.get_xaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#         for tick in ax.get_yaxis().get_major_ticks():
#             tick.set_pad(12.)
#             tick.label1 = tick._get_text1()
#
# =============================================================================
        xl = plt_ax.get_xlim()
        Dx = max(abs(xl[1]-xl[0]), 0.05)
        yl = plt_ax.get_ylim()
        Dy = max(abs(yl[1]-yl[0]), 0.05)

        plt_ax.set_xlim((xl[0]-Dx*0.02, max(xl[1]+Dx*0.02, 0)))
        plt_ax.set_ylim((yl[0]-Dy*0.02, yl[1] + Dy*0.02))

        return z, p, k

    # --------------------------------------------------------------------------
    def draw_contours(self, overlay):
        if overlay not in {"contour", "contourf"}:
            return
        self.ax.apply_aspect()  # normally, the correct aspect is only set when plotting
        xl = self.ax.get_xlim()
        yl = self.ax.get_ylim()
        # logger.warning(xl)
        # logger.warning(yl)

        [x, y] = np.meshgrid(
            np.arange(xl[0], xl[1], 0.01),
            np.arange(yl[0], yl[1], 0.01))
        z = x + 1j*y  # create coordinate grid for complex plane

        if self.but_log.isChecked():
            H_max = self.zmax_dB
            H_min = self.zmin_dB
        else:
            H_max = self.zmax
            H_min = self.zmin
        Hmag = H_mag(fb.fil[0]['ba'][0], fb.fil[0]['ba'][1], z, H_max, H_min=H_min,
                     log=self.but_log.isChecked())

        if overlay == "contour":
            self.ax.contour(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap)
        else:
            self.ax.contourf(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap)

        m_cb = cm.ScalarMappable(cmap=self.cmap)    # normalized proxy object that is
        m_cb.set_array(Hmag)                        # mappable for colorbar (?)
        self.col_bar = self.mplwidget.fig.colorbar(
            m_cb, ax=self.ax, shrink=1.0, aspect=40, pad=0.01, fraction=0.08)

        # Contour plots and color bar somehow mess up the coordinates:
        # restore to previous settings
        self.ax.set_xlim(xl)
        self.ax.set_xlim(yl)

    # --------------------------------------------------------------------------
    def draw_Hf(self, r=2, Hf_visible=True):
        """
        Draw the magnitude frequency response around the UC
        """
        self.diaRad_Hf.setVisible(Hf_visible)
        self.lblRad_Hf.setVisible(Hf_visible)
        if not Hf_visible:
            return

        # suppress "divide by zero in log10" warnings
        old_settings_seterr = np.seterr()
        np.seterr(divide='ignore')
        ba = fb.fil[0]['ba']
        w, H = sig.freqz(ba[0], ba[1], worN=params['N_FFT'], whole=True)
        H = np.abs(H)
        if self.but_log.isChecked():
            H = np.clip(np.log10(H), -6, None)  # clip to -120 dB
            H = H - np.max(H)  # shift scale to H_min ... 0
            H = 1 + (r-1) * (1 + H / abs(np.min(H)))  # scale to 1 ... r
        else:
            H = 1 + (r-1) * H / np.max(H)  # map |H(f)| to a range 1 ... r
        y = H * np.sin(w)
        x = H * np.cos(w)

        self.ax.plot(x, y, label="|H(f)|")
        uc = patches.Circle((0, 0), radius=r, fill=False,
                            color='grey', ls='dashed', zorder=1)
        self.ax.add_patch(uc)

        xl = self.ax.get_xlim()
        xmax = max(abs(xl[0]), abs(xl[1]), r*1.05)
        yl = self.ax.get_ylim()
        ymax = max(abs(yl[0]), abs(yl[1]), r*1.05)
        self.ax.set_xlim((-xmax, xmax))
        self.ax.set_ylim((-ymax, ymax))

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

    # incoming, connected in sender widget (locally connected to self.process_signals() )
    sig_rx = pyqtSignal(object)
#    sig_tx = pyqtSignal(object) # outgoing from process_signals

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

        self._construct_UI()

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

#------------------------------------------------------------------------------
    def _construct_UI(self):
        self.chkLog = QCheckBox("Log.", self)
        self.chkLog.setObjectName("chkLog")
        self.chkLog.setToolTip("Logarithmic scale")
        self.chkLog.setChecked(False)

        self.chkPolar = QCheckBox("Polar", self)
        self.chkPolar.setObjectName("chkPolar")
        self.chkPolar.setToolTip("Polar coordinate range")
        self.chkPolar.setChecked(False)

        self.lblBottom = QLabel("Bottom:", self)
        self.ledBottom = QLineEdit(self)
        self.ledBottom.setObjectName("ledBottom")
        self.ledBottom.setText(str(self.zmin))
        self.ledBottom.setToolTip("Minimum display value.")
        self.lblBottomdB = QLabel("dB", self)
        self.lblBottomdB.setVisible(self.chkLog.isChecked())
        
        self.lblTop = QLabel("Top:", self)
        self.ledTop = QLineEdit(self)
        self.ledTop.setObjectName("ledTop")
        self.ledTop.setText(str(self.zmax))
        self.ledTop.setToolTip("Maximum display value.")
        self.lblTopdB = QLabel("dB", self)
        self.lblTopdB.setVisible(self.chkLog.isChecked())
        
        self.chkUC = QCheckBox("UC", self)
        self.chkUC.setObjectName("chkUC")
        self.chkUC.setToolTip("Plot unit circle")
        self.chkUC.setChecked(True)

        self.chkPZ = QCheckBox("P/Z", self)
        self.chkPZ.setObjectName("chkPZ")
        self.chkPZ.setToolTip("Plot poles and zeros")
        self.chkPZ.setChecked(True)

        self.chkHf = QCheckBox("H(f)", self)
        self.chkHf.setObjectName("chkHf")
        self.chkHf.setToolTip("Plot H(f) along the unit circle")
        self.chkHf.setChecked(True)

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

        self.chkColormap_r = QCheckBox("reverse", self)
        self.chkColormap_r.setToolTip("reverse colormap")
        self.chkColormap_r.setChecked(True)

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

        self.chkColBar = QCheckBox("Colorbar", self)
        self.chkColBar.setObjectName("chkColBar")
        self.chkColBar.setToolTip("Show colorbar")
        self.chkColBar.setChecked(False)

        self.chkLighting = QCheckBox("Lighting", self)
        self.chkLighting.setObjectName("chkLighting")
        self.chkLighting.setToolTip("Enable light source")
        self.chkLighting.setChecked(False)

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

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

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

        #----------------------------------------------------------------------
        # LAYOUT for UI widgets
        #----------------------------------------------------------------------

        layGControls = QGridLayout()
        layGControls.addWidget(self.chkLog, 0, 0)
        layGControls.addWidget(self.chkPolar, 1, 0)
        layGControls.addWidget(self.lblTop, 0, 2)
        layGControls.addWidget(self.ledTop, 0, 4)
        layGControls.addWidget(self.lblTopdB, 0, 5)        
        layGControls.addWidget(self.lblBottom, 1, 2)
        layGControls.addWidget(self.ledBottom, 1, 4)
        layGControls.addWidget(self.lblBottomdB, 1, 5)   
        layGControls.setColumnStretch(5,1)

        layGControls.addWidget(self.chkUC, 0, 6)
        layGControls.addWidget(self.chkHf, 1, 6)
        layGControls.addWidget(self.chkPZ, 0, 8)

        layGControls.addWidget(self.cmbMode3D, 0, 10)
        layGControls.addWidget(self.chkContour2D, 1, 10)
        layGControls.addWidget(self.cmbColormap, 0,12,1,1)
        layGControls.addWidget(self.chkColormap_r, 1,12)

        layGControls.addWidget(self.chkLighting, 0, 14)
        layGControls.addWidget(self.chkColBar, 1, 14)

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

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

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

        #----------------------------------------------------------------------
        # mplwidget
        #----------------------------------------------------------------------
        # This is the plot pane widget, encompassing the other widgets
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

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

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

        self.chkPolar.clicked.connect(self._init_grid)
        self.chkUC.clicked.connect(self.draw)
        self.chkHf.clicked.connect(self.draw)
        self.chkPZ.clicked.connect(self.draw)
        self.cmbMode3D.currentIndexChanged.connect(self.draw)
        self.chkColBar.clicked.connect(self.draw)

        self.cmbColormap.currentIndexChanged.connect(self.draw)
        self.chkColormap_r.clicked.connect(self._init_cmb_colormap)

        self.chkLighting.clicked.connect(self.draw)
        self.diaAlpha.valueChanged.connect(self.draw)
        self.diaHatch.valueChanged.connect(self.draw)
        self.chkContour2D.clicked.connect(self.draw)

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

#------------------------------------------------------------------------------
    def _init_cmb_colormap(self):
        """ (Re-)Load combobox with available colormaps"""
        if self.chkColormap_r.isChecked():
            cmap_list = [m for m in cm.datad if m.endswith("_r")]
        else:
            cmap_list = [m for m in cm.datad if not m.endswith("_r")]
        # *_r colormaps reverse the color order
        cmap_list.sort()
        self.cmbColormap.blockSignals(True) # don't send signal "indexChanged"
        self.cmbColormap.clear()
        self.cmbColormap.addItems(cmap_list)
        self.cmbColormap.blockSignals(False)

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


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

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

        rmin = 0;    rmax = self.xmin  # polar range limits

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

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

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

        self.draw() # initial plot

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

        self._save_axes()

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

        self._restore_axes()

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

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

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


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


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

        self.draw()

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

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

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

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

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

        alpha = self.diaAlpha.value()/10.
        cmap = cm.get_cmap(str(self.cmbColormap.currentText()))
        # Number of Lines /step size for H(f) stride, mesh, contour3d:

        stride = 10 - self.diaHatch.value()
        NL = 3 * self.diaHatch.value() + 5

        surf_enabled = qget_cmb_box(self.cmbMode3D, data=False) in {'Surf', 'Contour'}
        self.cmbColormap.setEnabled(surf_enabled)
        self.chkColormap_r.setEnabled(surf_enabled)
        self.chkLighting.setEnabled(surf_enabled)
        self.chkColBar.setEnabled(surf_enabled)
        self.diaAlpha.setEnabled(surf_enabled or self.chkContour2D.isChecked())

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

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


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

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

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


        if self.chkLog.isChecked(): # logarithmic scale
            bottom = np.floor(max(self.zmin_dB, 20*log10(H_min)) / 10) * 10
            top = self.zmax_dB
            top_bottom = top - bottom

            zlevel = bottom - top_bottom * zlevel_rel

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

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

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

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

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


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

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

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

        #===============================================================
        ## plot Poles and Zeros
        #===============================================================
        if self.chkPZ.isChecked():

            PN_SIZE = 8 # size of P/N symbols

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

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

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

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

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

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


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

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

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

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

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

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

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 16
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):
        super().__init__()
        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.cmb_units_a_items = [
            "<span>Set unit for y-axis</span>",
            ("Auto", "Auto", "Use same setting as in Ripple specifications"),
            ("dB", "dB", "Attenuation in dB"),
            ("V", "V", "Linear gain"),
            ("W", "W", "Power gain")
        ]
        self.cmb_units_a_default = "auto"  # default setting

        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(to_html("Unit:", frmt="b"), self)

        self.cmb_units_a = QComboBox(self)
        qcmb_box_populate(self.cmb_units_a, self.cmb_units_a_items,
                          self.cmb_units_a_default)
        self.cmb_units_a.setObjectName("cmbUnitsA")

        self.lbl_log_bottom = QLabel(to_html("min =", 'bi'), self)
        self.led_log_bottom = QLineEdit(self)
        self.led_log_bottom.setText(str(self.log_bottom))
        self.led_log_bottom.setMaximumWidth(qtext_width(N_x=8))
        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.cmb_units_a.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.but_zerophase = PushButton(" Zero phase ", checked=False)
        self.but_zerophase.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(to_html("Inset", "bi"), 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.but_specs = PushButton("Specs ", checked=False)
        self.but_specs.setToolTip("Display filter specs as hatched regions")

        self.but_phase = PushButton("Phase ", checked=False)
        self.but_phase.setToolTip("Overlay phase")

        self.but_align = PushButton("Align", checked=True)
        self.but_align.setToolTip(
            "<span>Try to align gridlines for magnitude and phase "
            "(doesn't work in all cases).</span>")
        self.but_align.setVisible(self.but_phase.isChecked())

        # ----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        # ----------------------------------------------------------------------
        layHControls = QHBoxLayout()
        layHControls.addWidget(self.cmbShowH)
        layHControls.addWidget(self.lblIn)
        layHControls.addWidget(self.cmb_units_a)
        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.but_zerophase)
        layHControls.addStretch(1)
        layHControls.addWidget(self.lblInset)
        layHControls.addWidget(self.cmbInset)
        layHControls.addStretch(1)
        layHControls.addWidget(self.but_specs)
        layHControls.addStretch(1)
        layHControls.addWidget(self.but_phase)
        layHControls.addWidget(self.but_align)
        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['mpl_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.cmb_units_a.currentIndexChanged.connect(self.draw)
        self.led_log_bottom.editingFinished.connect(self.update_view)
        self.cmbShowH.currentIndexChanged.connect(self.draw)

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

        self.but_specs.clicked.connect(self.draw)
        self.but_phase.clicked.connect(self.draw)
        self.but_align.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.but_specs.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.but_phase.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_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.but_align.setVisible(self.but_phase.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.cmb_units_a.currentText() == 'Auto':
            self.unitA = fb.fil[0]['amp_specs_unit']
        else:
            self.unitA = self.cmb_units_a.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.but_zerophase.setCheckable(self.unitA == 'V')
        self.but_zerophase.setEnabled(self.unitA == 'V')

        self.specs = self.but_specs.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.but_zerophase.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.but_specs.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.but_phase.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.but_align.isChecked():
            # Align gridlines between H(f) and phi nicely
            self.align_y_axes(self.ax, self.ax_p)
        self.mplwidget.redraw()
Exemplo n.º 17
0
class Plot_Phi(QWidget):
    # incoming, connected in sender widget (locally connected to self.process_sig_rx() )
    sig_rx = pyqtSignal(object)

    def __init__(self, parent):
        super(Plot_Phi, self).__init__(parent)
        self.needs_calc = True  # recalculation of filter function necessary
        self.needs_draw = True  # plotting neccessary (e.g. log instead of  lin)
        self.needs_redraw = True  # redraw (e.g. because size has changed) neccessary
        self.tool_tip = "Phase frequency response"
        self.tab_label = "\u03C6(f)"  # phi(f)
        self._construct_UI()

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

    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | needs_calc = {1}, visible = {2}"\
                     .format(dict_sig, self.needs_calc, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc:
                self.draw()
                self.needs_calc = False
                self.needs_draw = False
            elif 'view_changed' in dict_sig or self.needs_draw:
                self.update_view()
                self.needs_draw = False
            elif ('ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized')\
                or self.needs_redraw:
                self.redraw()
        else:
            if 'data_changed' in dict_sig:
                self.needs_calc = True
            elif 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig[
                    'ui_changed'] == 'resized':
                self.needs_redraw = True

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """

        self.cmbUnitsPhi = QComboBox(self)
        units = ["rad", "rad/pi", "deg"]
        scales = [1., 1. / np.pi, 180. / np.pi]
        for unit, scale in zip(units, scales):
            self.cmbUnitsPhi.addItem(unit, scale)
        self.cmbUnitsPhi.setObjectName("cmbUnitsA")
        self.cmbUnitsPhi.setToolTip("Set unit for phase.")
        self.cmbUnitsPhi.setCurrentIndex(0)
        self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkWrap = QCheckBox("Wrapped Phase", self)
        self.chkWrap.setChecked(False)
        self.chkWrap.setToolTip("Plot phase wrapped to +/- pi")

        layHControls = QHBoxLayout()
        layHControls.addWidget(self.cmbUnitsPhi)
        layHControls.addWidget(self.chkWrap)
        layHControls.addStretch(10)

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

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets
        #----------------------------------------------------------------------
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self.draw()  # initial drawing

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_sig_rx)

        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.chkWrap.clicked.connect(self.draw)
        self.cmbUnitsPhi.currentIndexChanged.connect(self.draw)
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)

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

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

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

    def calc_resp(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        # calculate H_cplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0],
                                             params['N_FFT'],
                                             wholeF=True)
        # replace nan and inf by finite values, otherwise np.unwrap yields
        # an array full of nans
        self.H_cmplx = np.nan_to_num(self.H_cmplx)

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

    def draw(self):
        """
        Main entry point:
        Re-calculate \|H(f)\| and draw the figure
        """
        self.calc_resp()
        self.update_view()

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

    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """

        self.unitPhi = qget_cmb_box(self.cmbUnitsPhi, data=False)

        f_S2 = fb.fil[0]['f_S'] / 2.

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

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

        y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in '
        if self.unitPhi == 'rad':
            y_str += 'rad ' + r'$\rightarrow $'
            scale = 1.
        elif self.unitPhi == 'rad/pi':
            y_str += 'rad' + r'$ / \pi \;\rightarrow $'
            scale = 1. / np.pi
        else:
            y_str += 'deg ' + r'$\rightarrow $'
            scale = 180. / np.pi
        fb.fil[0]['plt_phiLabel'] = y_str
        fb.fil[0]['plt_phiUnit'] = self.unitPhi

        if self.chkWrap.isChecked():
            phi_plt = np.angle(H) * scale
        else:
            phi_plt = np.unwrap(np.angle(H)) * scale

        #---------------------------------------------------------
        self.ax.clear()  # need to clear, doesn't overwrite
        line_phi, = self.ax.plot(F, phi_plt)
        #---------------------------------------------------------

        self.ax.set_title(r'Phase Frequency Response')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(y_str)
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

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

    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 18
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.data_changed = True   # flag whether plot needs to be updated
        self.ui_changed = True   # flag whether plot needs to be updated        
        self.needs_redraw = True # flag whether plot needs to be redrawn
        self.tool_tip = "Magnitude and phase frequency response"
        self.tab_label = "|H(f)|"
        self._construct_ui()

#------------------------------------------------------------------------------
    def process_sig_rx(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\
                     .format(dict_sig, self.data_changed, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\
                    or 'home' in dict_sig or self.data_changed:
                self.draw()
                self.data_changed = False
                self.view_changed = False
                self.ui_changed = False
            if 'view_changed' in dict_sig or self.view_changed:
                self.update_view()
                self.view_changed = False
                self.ui_changed = False               
            if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\
                    or self.ui_changed:
                self.redraw()
                self.ui_changed = False
        else: 
            if 'data_changed' in dict_sig or 'specs_changed' in dict_sig:
                self.data_changed = True
            if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.ui_changed = True
            if 'view_changed' in dict_sig:
                self.view_changed = 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.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkLinphase = QCheckBox("Zero phase", self)
        self.chkLinphase.setToolTip("<span>Subtract linear phase according to 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("Show 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")

        #----------------------------------------------------------------------
        #               ### 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.chkLinphase)
        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.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.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.cmbShowH.currentIndexChanged.connect(self.draw)

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

        self.chkSpecs.clicked.connect(self.draw)
        self.chkPhase.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.add_subplot(111)
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    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_S/2
        F_PB  = self.F_PB
        F_SB  = fb.fil[0]['F_SB'] * self.f_S
        F_SB2 = fb.fil[0]['F_SB2'] * self.f_S
        F_PB2 = fb.fil[0]['F_PB2'] * self.f_S

        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_S - F_lim_upl
                F_lim_lol = self.f_S - F_lim_lol
                F_lim_upc = self.f_S - F_lim_upc
                F_lim_loc = self.f_S - F_lim_loc
                F_lim_upr = self.f_S - F_lim_upr
                F_lim_lor = self.f_S - 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.specs:
                    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
        """
        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)
            nbins = len(self.ax.get_yticks()) # number of ticks on main y-axis
            # 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))
#        else:
#            try:
#                self.mplwidget.fig.delaxes(self.ax_p)
#            except (KeyError, AttributeError):
#                pass
#        self.draw()

#------------------------------------------------------------------------------
    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.calc_hf()
        self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        # 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()

        # Linphase settings only makes sense for amplitude plot
        self.chkLinphase.setCheckable(self.unitA == 'V')
        self.chkLinphase.setEnabled(self.unitA == 'V')

        self.specs = self.chkSpecs.isChecked()
        self.linphase = self.chkLinphase.isChecked()

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

        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_S

        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_S/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.linphase: # 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':
                A_lim = [20*np.log10(A_min) -10, 20*np.log10(1+A_max) +1]
                self.H_plt = 20*np.log10(abs(H))
                H_str += ' in dB ' + r'$\rightarrow$'
            elif self.unitA == 'V': #  'lin'
                A_lim = [0, (1.05 + A_max)]
                self.H_plt = H
                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 $'

            #-----------------------------------------------------------
            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.specs: 
                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)

            self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
            self.ax.set_ylabel(H_str)
            self.ax.set_title(r'Magnitude Frequency Response')            

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

    def __init__(self, parent):
        super(PlotPhi, self).__init__(parent)
        self._construct_UI()

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements
        """

        self.cmbUnitsPhi = QComboBox(self)
        units = ["rad", "rad/pi",  "deg"]
        scales = [1.,   1./ np.pi, 180./np.pi]
        for unit, scale in zip(units, scales):
            self.cmbUnitsPhi.addItem(unit, scale)
        self.cmbUnitsPhi.setObjectName("cmbUnitsA")
        self.cmbUnitsPhi.setToolTip("Set unit for phase.")
        self.cmbUnitsPhi.setCurrentIndex(0)
        self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        self.chkWrap = QCheckBox("Wrapped Phase", self)
        self.chkWrap.setChecked(False)
        self.chkWrap.setToolTip("Plot phase wrapped to +/- pi")
        
        layHControls = QHBoxLayout()
#        layHControls.addStretch(10)
        layHControls.addWidget(self.cmbUnitsPhi)
        layHControls.addWidget(self.chkWrap)
        layHControls.addStretch(10)
        
        #----------------------------------------------------------------------
        #               ### frmControls ###
        #
        # This widget encompasses all control subwidgets
        #----------------------------------------------------------------------
        self.frmControls = QFrame(self)
        self.frmControls.setObjectName("frmControls")
        self.frmControls.setLayout(layHControls)

        #----------------------------------------------------------------------
        #               ### mplwidget ###
        #
        # main widget, encompassing the other widgets 
        #----------------------------------------------------------------------  
        self.mplwidget = MplWidget(self)
        self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()

        self.draw() # initial drawing

#        #=============================================
#        # Signals & Slots
#        #=============================================
        self.chkWrap.clicked.connect(self.draw)
        self.cmbUnitsPhi.currentIndexChanged.connect(self.draw)
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)

#------------------------------------------------------------------------------
    @pyqtSlot(object)
    def process_signals(self, sig_dict):
        """
        Process signals coming from the navigation toolbar
        """
        if 'update_view' in sig_dict:
            self.update_view()
        elif 'enabled' in sig_dict:
            self.enable_ui(sig_dict['enabled'])
        elif 'home' in sig_dict:
            self.draw()
        else:
            pass

#------------------------------------------------------------------------------
    def enable_ui(self, enabled):
        """
        Triggered when the toolbar is enabled or disabled
        """
        self.frmControls.setEnabled(enabled)
        if enabled:
            self.init_axes()
            self.draw()

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes
        """
#        self.ax = self.mplwidget.ax
        self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.clear()
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    def calc_hf(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        # calculate H_cplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], wholeF=True)
        # replace nan and inf by finite values, otherwise np.unwrap yields
        # an array full of nans
        self.H_cmplx = np.nan_to_num(self.H_cmplx) 

#------------------------------------------------------------------------------
    def draw(self):
        """
        Main entry point: 
        Re-calculate |H(f)| and draw the figure if enabled
        """
        if self.mplwidget.mplToolbar.enabled:
            self.calc_hf()
            self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """

        self.unitPhi = self.cmbUnitsPhi.currentText()

        f_S2 = fb.fil[0]['f_S'] / 2.

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

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

        y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in '
        if self.unitPhi == 'rad':
            y_str += 'rad ' + r'$\rightarrow $'
            scale = 1.
        elif self.unitPhi == 'rad/pi':
            y_str += 'rad' + r'$ / \pi \;\rightarrow $'
            scale = 1./ np.pi
        else:
            y_str += 'deg ' + r'$\rightarrow $'
            scale = 180./np.pi
        fb.fil[0]['plt_phiLabel'] = y_str
        fb.fil[0]['plt_phiUnit'] = self.unitPhi
        
        if self.chkWrap.isChecked():
            phi_plt = np.angle(H) * scale
        else:
            phi_plt = np.unwrap(np.angle(H)) * scale

        #---------------------------------------------------------
        self.ax.clear() # need to clear, doesn't overwrite 
        line_phi, = self.ax.plot(F, phi_plt)
        #---------------------------------------------------------

        self.ax.set_title(r'Phase Frequency Response')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(y_str)
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()
        
#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()
Exemplo n.º 20
0
class Plot_Tau_G(QWidget):
    """
    Widget for plotting the group delay
    """
    # incoming, connected in sender widget (locally connected to self.process_signals() )
    sig_rx = pyqtSignal(object)
#    sig_tx = pyqtSignal(object) # outgoing from process_signals


    def __init__(self, parent):
        super(Plot_Tau_G, self).__init__(parent)
        self.verbose = True # suppress warnings
        self.needs_draw = True   # flag whether plot needs to be updated
        self.needs_redraw = True # flag whether plot needs to be redrawn
        self.tool_tip = "Group delay"
        self.tab_label = "tau_g"
        self._construct_UI()

    def _construct_UI(self):
        """
        Intitialize the widget, consisting of:
        - Matplotlib widget with NavigationToolbar
        - Frame with control elements (currently commented out)
        """

# =============================================================================
# #### not needed at the moment ###
#        self.chkWarnings = QCheckBox("Enable Warnings", self)
#        self.chkWarnings.setChecked(False)
#        self.chkWarnings.setToolTip("Print warnings about singular group delay")
#
#        self.chkScipy = QCheckBox("Scipy", self)
#        self.chkScipy.setChecked(False)
#        self.chkScipy.setToolTip("Use scipy group delay routine")
#    
#        layHControls = QHBoxLayout()
#        layHControls.addStretch(10)
#        layHControls.addWidget(self.chkWarnings)
#        layHControls.addWidget(self.chkScipy)
#    
#        # This widget encompasses all control subwidgets:
#        self.frmControls = QFrame(self)
#        self.frmControls.setObjectName("frmControls")
#        self.frmControls.setLayout(layHControls)
# =============================================================================
        self.mplwidget = MplWidget(self)
        #self.mplwidget.layVMainMpl.addWidget(self.frmControls)
        self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins'])
        self.setLayout(self.mplwidget.layVMainMpl)

        self.init_axes()
        self.draw() # initial drawing of tau_g

        #----------------------------------------------------------------------
        # GLOBAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.sig_rx.connect(self.process_signals)
        #----------------------------------------------------------------------
        # LOCAL SIGNALS & SLOTs
        #----------------------------------------------------------------------
        self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)

#------------------------------------------------------------------------------
    def process_signals(self, dict_sig=None):
        """
        Process signals coming from the navigation toolbar and from sig_rx
        """
        logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\
                     .format(dict_sig, self.needs_draw, self.isVisible()))
        if self.isVisible():
            if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_draw:
                self.draw()
                self.needs_draw = False
                self.needs_redraw = False
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\
                    or self.needs_redraw:
                self.redraw()
                self.needs_redraw = False
            elif 'view_changed' in dict_sig:
                self.update_view()
        else:
            if 'data_changed' in dict_sig or 'view_changed' in dict_sig:
                self.needs_draw = True
            elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized':
                self.needs_redraw = True

#------------------------------------------------------------------------------
    def init_axes(self):
        """
        Initialize and clear the axes
        """
        self.ax = self.mplwidget.fig.add_subplot(111)
        self.ax.get_xaxis().tick_bottom() # remove axis ticks on top
        self.ax.get_yaxis().tick_left() # remove axis ticks right

#------------------------------------------------------------------------------
    def calc_tau_g(self):
        """
        (Re-)Calculate the complex frequency response H(f)
        """
        bb = fb.fil[0]['ba'][0]
        aa = fb.fil[0]['ba'][1]

        # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi:
        self.W, self.tau_g = group_delay((bb, aa), w=params['N_FFT'], whole = True)
            #verbose = self.verbose) # self.chkWarnings.isChecked())

        # Zero phase filters have no group delay (Causal+AntiCausal)
        if 'baA' in fb.fil[0]:
           self.tau_g = np.zeros(self.tau_g.size)


#------------------------------------------------------------------------------
    def draw(self):
        self.calc_tau_g()
        self.update_view()

#------------------------------------------------------------------------------
    def update_view(self):
        """
        Draw the figure with new limits, scale etc without recalculating H(f)
        """
        #========= select frequency range to be displayed =====================
        #=== shift, scale and select: W -> F, H_cplx -> H_c
        f_S2 = fb.fil[0]['f_S'] / 2.
        F = self.W * f_S2 / np.pi

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

        #================ Main Plotting Routine =========================
        #===  clear the axes and (re)draw the plot

        if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $'
        else:
            tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\
                + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $'
            tau_g = tau_g / fb.fil[0]['f_S']

        #---------------------------------------------------------
        self.ax.clear() # need to clear, doesn't overwrite
        line_tau_g, = self.ax.plot(F, tau_g, label = "Group Delay")
        #---------------------------------------------------------

        self.ax.set_title(r'Group Delay $ \tau_g$')
        self.ax.set_xlabel(fb.fil[0]['plt_fLabel'])
        self.ax.set_ylabel(tau_str)
        # widen y-limits to suppress numerical inaccuracies when tau_g = constant
        self.ax.set_ylim([max(min(tau_g)-0.5,0), max(tau_g) + 0.5])
        self.ax.set_xlim(fb.fil[0]['freqSpecsRange'])

        self.redraw()

#------------------------------------------------------------------------------
    def redraw(self):
        """
        Redraw the canvas when e.g. the canvas size has changed
        """
        self.mplwidget.redraw()