示例#1
0
class PlotImpz_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming: not implemented at the moment, update_N is triggered directly
    # by plot_impz
    # sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)
    # outgoing: to fft related widgets (FFT window widget, qfft_win_select)
    sig_tx_fft = pyqtSignal(object)

    from pyfda.libs.pyfda_qt_lib import emit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        layH_ctrl_time.addStretch(10)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        This method is called by:

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

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

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

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

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

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

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

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

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

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

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

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

        return N
示例#2
0
文件: delay.py 项目: xfunture/pyfda
class Delay(QWidget):

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

    info ="""
**Delay widget**

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

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

    """

    sig_tx = pyqtSignal(object)

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

        self.N = 5

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

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

        self.info_doc = []

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

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

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

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

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


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


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

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

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


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

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


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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        layH_ctrl_stim_par.addLayout(layGStim)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

        n_par = self.win_dict['n_par']

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

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

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


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

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

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

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

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

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


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

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

        return N
示例#4
0
文件: firwin.py 项目: tspiteri/pyfda
class Firwin(QWidget):

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

    sig_tx = pyqtSignal(
        object)  # local signal between FFT widget and FFTWin_Selector
    sig_tx_local = pyqtSignal(object)
    from pyfda.libs.pyfda_qt_lib import emit

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

        self.ft = 'FIR'

        win_names_list = [
            "Boxcar", "Rectangular", "Barthann", "Bartlett", "Blackman",
            "Blackmanharris", "Bohman", "Cosine", "Dolph-Chebyshev", "Flattop",
            "General Gaussian", "Gauss", "Hamming", "Hann", "Kaiser",
            "Nuttall", "Parzen", "Slepian", "Triangular", "Tukey"
        ]
        self.cur_win_name = "Kaiser"  # set initial window type
        self.alg = "ichige"

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

        # get initial / last setting from dictionary, updating self.win_dict
        self._load_dict()

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

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

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

        self.info = """**Windowed FIR filters**

        are designed by truncating the
        infinite impulse response of an ideal filter with a window function.
        The kind of used window has strong influence on ripple etc. of the
        resulting filter.

        **Design routines:**

        ``scipy.signal.firwin()``

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

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

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

    def process_sig_rx(self, dict_sig=None):
        """
        Process local signals from / for
        - FFT window widget
        - qfft_win_select
        """

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

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

        # --- signals coming from the FFT window widget or the qfft_win_select
        if dict_sig['class'] in {'Plot_FFT_win', 'QFFTWinSelector'}:
            if 'closeEvent' in dict_sig:  # hide FFT window windget and return
                self.hide_fft_wdg()
                return
            else:
                if 'view_changed' in dict_sig and 'fft_win' in dict_sig[
                        'view_changed']:
                    # self._update_fft_window()  # TODO: needed?
                    # local connection to FFT window widget and qfft_win_select
                    self.emit(dict_sig, sig_name='sig_tx_local')
                    # global connection to upper hierachies
                    # send notification that filter design has changed
                    self.emit({'filt_changed': 'firwin'})

    # --------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in
        select_filter.py using the handle to the filter object, fb.filObj .
        """
        # Combobox for selecting the algorithm to estimate minimum filter order
        self.cmb_firwin_alg = QComboBox(self)
        self.cmb_firwin_alg.setObjectName('wdg_cmb_firwin_alg')
        self.cmb_firwin_alg.addItems(['ichige', 'kaiser', 'herrmann'])
        # Minimum size, can be changed in the upper hierarchy levels using layouts:
        self.cmb_firwin_alg.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.cmb_firwin_alg.hide()

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

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

        self.layHWin1 = QHBoxLayout()
        # self.layHWin1.addWidget(self.cmb_firwin_win)
        # self.layHWin1.addWidget(self.but_fft_wdg)
        self.layHWin1.addWidget(self.cmb_firwin_alg)
        self.layHWin2 = QHBoxLayout()
        self.layHWin2.addWidget(self.but_fft_wdg)
        self.layHWin2.addWidget(self.qfft_win_select)

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

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

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

        # ----------------------------------------------------------------------
        # SIGNALS & SLOTs
        # ----------------------------------------------------------------------
        self.cmb_firwin_alg.currentIndexChanged.connect(
            self._update_fft_window)
        self.but_fft_wdg.clicked.connect(self.toggle_fft_wdg)
        # ----------------------------------------------------------------------

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

    def _update_fft_window(self):
        """ Update window type for FirWin - unneeded at the moment """
        self.alg = str(self.cmb_firwin_alg.currentText())
        self.emit({'filt_changed': 'firwin'})

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

        self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_local')

    # --------------------------------------------------------------------------
    def _store_dict(self):
        """
        Store window and parameter settings using `self.win_dict` in filter dictionary.
        """
        if 'wdg_fil' not in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update({'firwin': self.win_dict})

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

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

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

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

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

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

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

    def firwin(self,
               numtaps,
               cutoff,
               window=None,
               pass_zero=True,
               scale=True,
               nyq=1.0,
               fs=None):
        """
        FIR filter design using the window method. This is more or less the
        same as `scipy.signal.firwin` with the exception that an ndarray with
        the window values can be passed as an alternative to the window name.

        The parameters "width" (specifying a Kaiser window) and "fs" have been
        omitted, they are not needed here.

        This function computes the coefficients of a finite impulse response
        filter.  The filter will have linear phase; it will be Type I if
        `numtaps` is odd and Type II if `numtaps` is even.
        Type II filters always have zero response at the Nyquist rate, so a
        ValueError exception is raised if firwin is called with `numtaps` even and
        having a passband whose right end is at the Nyquist rate.

        Parameters
        ----------
        numtaps : int
            Length of the filter (number of coefficients, i.e. the filter
            order + 1).  `numtaps` must be even if a passband includes the
            Nyquist frequency.
        cutoff : float or 1D array_like
            Cutoff frequency of filter (expressed in the same units as `nyq`)
            OR an array of cutoff frequencies (that is, band edges). In the
            latter case, the frequencies in `cutoff` should be positive and
            monotonically increasing between 0 and `nyq`.  The values 0 and
            `nyq` must not be included in `cutoff`.
        window : ndarray or string
            string: use the window with the passed name from scipy.signal.windows

            ndarray: The window values - this is an addition to the original
            firwin routine.
        pass_zero : bool, optional
            If True, the gain at the frequency 0 (i.e. the "DC gain") is 1.
            Otherwise the DC gain is 0.
        scale : bool, optional
            Set to True to scale the coefficients so that the frequency
            response is exactly unity at a certain frequency.
            That frequency is either:
            - 0 (DC) if the first passband starts at 0 (i.e. pass_zero
              is True)
            - `nyq` (the Nyquist rate) if the first passband ends at
              `nyq` (i.e the filter is a single band highpass filter);
              center of first passband otherwise
        nyq : float, optional
            Nyquist frequency.  Each frequency in `cutoff` must be between 0
            and `nyq`.

        Returns
        -------
        h : (numtaps,) ndarray
            Coefficients of length `numtaps` FIR filter.
        Raises
        ------
        ValueError
            If any value in `cutoff` is less than or equal to 0 or greater
            than or equal to `nyq`, if the values in `cutoff` are not strictly
            monotonically increasing, or if `numtaps` is even but a passband
            includes the Nyquist frequency.
        See also
        --------
        scipy.firwin
        """
        cutoff = np.atleast_1d(cutoff) / float(nyq)

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

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

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

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

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

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

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

    def _firwin_ord(self, F, W, A, alg):
        # http://www.mikroe.com/chapters/view/72/chapter-2-fir-filters/
        delta_f = abs(F[1] - F[0]) * 2  # referred to f_Ny
        # delta_A = np.sqrt(A[0] * A[1])
        if "Kaiser" in self.win_dict and self.win_dict[
                'cur_win_name'] == "Kaiser":
            N, beta = sig.kaiserord(20 * np.log10(np.abs(fb.fil[0]['A_SB'])),
                                    delta_f)
            # logger.warning(f"N={N}, beta={beta}, A_SB={fb.fil[0]['A_SB']}")
            self.win_dict["Kaiser"]["par"][0]["val"] = beta
            self.qfft_win_select.led_win_par_0.setText(str(beta))
            self.qfft_win_select.ui2dict_params(
            )  # pass changed parameter to other widgets
        else:
            N = remezord(F, W, A, fs=1, alg=alg)[0]
        self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_local')
        return N

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.N = self._firwin_ord([self.F_PB, self.F_SB], [1, 0],
                                  [self.A_PB, self.A_SB],
                                  alg=self.alg)
        if not self._test_N():
            return -1

        fil_dict['F_C'] = (self.F_SB +
                           self.F_PB) / 2  # average calculated F_PB and F_SB
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        nyq=0.5,
                        window=self.qfft_win_select.get_window(self.N,
                                                               sym=True)))

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        logger.warning(self.win_dict["cur_win_name"])
        self._save(
            fil_dict,
            self.firwin(self.N,
                        fil_dict['F_C'],
                        nyq=0.5,
                        window=self.qfft_win_select.get_window(self.N,
                                                               sym=True)))

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

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

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

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

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

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

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

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

    # --------------------------------------------------------------------------
    def hide_fft_wdg(self):
        """
        The closeEvent caused by clicking the "x" in the FFT widget is caught
        there and routed here to only hide the window
        """
        self.but_fft_wdg.setChecked(False)
        self.fft_widget.hide()
示例#5
0
文件: firwin.py 项目: mfkiwl/pyfda
class Firwin(QWidget):

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

    sig_tx = pyqtSignal(object)

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

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

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

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

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

        ``scipy.signal.firwin()``

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return h

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

        return N

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

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

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

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

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

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

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

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

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

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

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

    def close_fft_win(self):
        self.fft_window = None
        self.but_fft_win.setChecked(False)
        qstyle_widget(self.but_fft_win, "normal")
示例#6
0
class EllipZeroPhz(QWidget):

#    Since we are also using poles/residues -> let's force zpk
#    if SOS_AVAIL:
#       output format of filter design routines 'zpk' / 'ba' / 'sos'
#        FRMT = 'sos' 
#    else:
    FRMT = 'zpk'
        
    info = """
**Elliptic filters with zero phase**

(also known as Cauer filters) have the steepest rate of transition between the 
frequency response’s passband and stopband of all IIR filters. This comes
at the expense of a constant ripple (equiripple) :math:`A_PB` and :math:`A_SB`
in both pass and stop band. Ringing of the step response is increased in
comparison to Chebychev filters.
 
As the passband ripple :math:`A_PB` approaches 0, the elliptical filter becomes
a Chebyshev type II filter. As the stopband ripple :math:`A_SB` approaches 0,
it becomes a Chebyshev type I filter. As both approach 0, becomes a Butterworth
filter (butter).

For the filter design, the order :math:`N`, minimum stopband attenuation
:math:`A_SB` and the critical frequency / frequencies :math:`F_PB` where the 
gain first drops below the maximum passband ripple :math:`-A_PB` have to be specified.

The ``ellipord()`` helper routine calculates the minimum order :math:`N` and 
critical passband frequency :math:`F_C` from pass and stop band specifications.

The Zero Phase Elliptic Filter squares an elliptic filter designed in
a way to produce the required Amplitude specifications. So initially the
amplitude specs design an elliptic filter with the square root of the amp specs.
The filter is then squared to produce a zero phase filter.
The filter coefficients are applied to the signal data in a backward and forward
time fashion.  This filter can only be applied to stored signal data (not
real-time streaming data that comes in a forward time order).

We are forcing the order N of the filter to be even.  This simplifies the poles/zeros
to be complex (no real values).

**Design routines:**

``scipy.signal.ellip()``, ``scipy.signal.ellipord()``

        """
    sig_tx = pyqtSignal(object)

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

        self.ft = 'IIR'

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

        self.rt_dict_add = {
            'COM':{'man':{'msg':('a',
             "Enter the filter order <b><i>N</i></b>, the minimum stop "
             "band attenuation <b><i>A<sub>SB</sub></i></b> and frequency or "
             "frequencies <b><i>F<sub>C</sub></i></b>  where gain first drops "
             "below the max passband ripple <b><i>-A<sub>PB</sub></i></b> .")}},
            'LP': {'man':{}, 'min':{}},
            'HP': {'man':{}, 'min':{}},
            'BS': {'man':{}, 'min':{}},
            'BP': {'man':{}, 'min':{}},
            }

        self.info_doc = []
        self.info_doc.append('ellip()\n========')
        self.info_doc.append(sig.ellip.__doc__)
        self.info_doc.append('ellipord()\n==========')
        self.info_doc.append(ellipord.__doc__)

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        self.chkComplex   = QCheckBox("ComplexFilter", self)
        self.chkComplex.setToolTip("Designs BP or BS Filter for complex data.")
        self.chkComplex.setObjectName('wdg_lbl_el')
        self.chkComplex.setChecked(False)

        #--------------------------------------------------
        #  Layout for filter optional subwidgets
        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        #self.layHWin.addWidget(self.chkComplex)
        self.layHWin.addStretch() 
        self.layHWin.setContentsMargins(0,0,0,0)

        # Widget containing all subwidgets
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        For zero phase filter, we take square root of amplitude specs
        since we later square filter.  Define design around smallest amp spec
        """
        # Frequencies are normalized to f_Nyq = f_S/2, ripple specs are in dB
        self.analog = False # set to True for analog filters
        self.manual = False # default is normal design
        self.N     = int(fil_dict['N'])

        # force N to be even
        if (self.N % 2) == 1:
            self.N += 1
        self.F_PB  = fil_dict['F_PB'] * 2
        self.F_SB  = fil_dict['F_SB'] * 2
        self.F_PB2 = fil_dict['F_PB2'] * 2
        self.F_SB2 = fil_dict['F_SB2'] * 2
        self.F_PBC = None

        # find smallest spec'd linear value and rewrite dictionary
        ampPB = fil_dict['A_PB']
        ampSB = fil_dict['A_SB']

        # take square roots of amp specs so resulting squared
        # filter will meet specifications
        if (ampPB < ampSB):
            ampSB = sqrt(ampPB)
            ampPB = sqrt(1+ampPB)-1
        else:
            ampPB = sqrt(1+ampSB)-1
            ampSB = sqrt(ampSB)
        self.A_PB = lin2unit(ampPB, 'IIR', 'A_PB', unit='dB')
        self.A_SB = lin2unit(ampSB, 'IIR', 'A_SB', unit='dB')
        #logger.warning("design with "+str(self.A_PB)+","+str(self.A_SB))

        # ellip filter routines support only one amplitude spec for
        # pass- and stop band each
        if str(fil_dict['rt']) == 'BS':
            fil_dict['A_PB2'] = self.A_PB
        elif str(fil_dict['rt']) == 'BP':
            fil_dict['A_SB2'] = self.A_SB

#   partial fraction expansion to define residue vector
    def _partial(self, k, p, z, norder):
        # create diff array
        diff = p - z

        # now compute residual vector
        cone = complex(1.,0.)
        residues = zeros(norder, complex)
        for i in range(norder):
            residues[i] =  k * (diff[i] / p[i])
            for j in range(norder):
                if (j != i):
                    residues[i] = residues[i] * (cone + diff[j]/(p[i] - p[j]))

        # now compute DC term for new expansion
        sumRes = 0.
        for i in range(norder):
            sumRes = sumRes + residues[i].real

        dc = k - sumRes

        return (dc, residues)

#
# Take a causal filter and square it. The result has the square
#  of the amplitude response of the input, and zero phase. Filter
#  is noncausal.
# Input:
#   k - gain in pole/zero form
#   p - numpy array of poles
#   z - numpy array of zeros
#   g - gain in pole/residue form
#   r - numpy array of residues
#   nn- order of filter

# Output:
#   kn - new gain (pole/zero)
#   pn - new poles
#   zn - new zeros  (numpy array)
#   gn - new gain (pole/residue)
#   rn - new residues

    def _sqCausal (self, k, p, z, g, r, nn):

#       Anticausal poles have conjugate-reciprocal symmetry
#       Starting anticausal residues are conjugates (adjusted below)

        pA = conj(1./p)   # antiCausal poles
        zA = conj(z)      # antiCausal zeros (store reciprocal)
        rA = conj(r)      # antiCausal residues (to start)
        rC = zeros(nn, complex)

#       Adjust residues. Causal part first.
        for j in range(nn):

#           Evaluate the anticausal filter at each causal pole
            tmpx = rA / (1. - p[j]/pA)
            ztmp = g + sum(tmpx)

#           Adjust residue
            rC[j] = r[j]*ztmp

#       anticausal residues are just conjugates of causal residues
#        r3 = np.conj(r2)

#       Compute the constant term
        dc2 = (g + sum(r))*g - sum(rC)

#       Populate output (2nn elements)
        gn = dc2.real

#       Drop complex poles/residues in LHP, keep only UHP

        pA = conj(p)  #store AntiCasual pole (reciprocal)
        p0 = zeros(int(nn/2), complex)
        r0 = zeros(int(nn/2), complex)
        cnt = 0
        for j in range(nn):
            if (p[j].imag > 0.0):
                p0[cnt] = p[j]
                r0[cnt] = rC[j]
                cnt = cnt+1

#       Let operator know we squared filter
#        logger.info('After squaring filter, order: '+str(nn*2))

#       For now and our case, only store causal residues
#       Filters are symmetric and can generate antiCausal residues
        return (pA, zA, gn, p0, r0)


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

#   custom save of filter dictionary
    def _save(self, fil_dict, arg):
        """
        First design initial elliptic filter meeting sqRoot Amp specs;
         - Then create residue vector from poles/zeros;
         - Then square filter (k,p,z and dc,p,r) to get zero phase filter;
         - Then Convert results of filter design to all available formats (pz, pr, ba, sos)
        and store them in the global filter dictionary.

        Corner frequencies and order calculated for minimum filter order are
        also stored to allow for an easy subsequent manual filter optimization.
        """
        fil_save(fil_dict, arg, self.FRMT, __name__)

        # For min. filter order algorithms, update filter dict with calculated
        # new values for filter order N and corner frequency(s) F_PBC

        fil_dict['N'] = self.N
        if str(fil_dict['fo']) == 'min':
            if str(fil_dict['rt']) == 'LP' or str(fil_dict['rt']) == 'HP':
#               HP or LP - single  corner frequency
                fil_dict['F_PB'] = self.F_PBC / 2.
            else: # BP or BS - two corner frequencies
                fil_dict['F_PB'] = self.F_PBC[0] / 2.
                fil_dict['F_PB2'] = self.F_PBC[1] / 2.

#       Now generate poles/residues for custom file save of new parameters
        if (not self.manual):
            z = fil_dict['zpk'][0]
            p = fil_dict['zpk'][1]
            k = fil_dict['zpk'][2]
            n = len(z)
            gain, residues = self._partial (k, p, z, n)

            pA, zA, gn, pC, rC = self._sqCausal (k, p, z, gain, residues, n)
            fil_dict['rpk'] = [rC, pC, gn]

#           save antiCausal b,a (nonReciprocal) also [easier to compute h(n)
            try:
               fil_dict['baA'] = sig.zpk2tf(zA, pA, k)
            except Exception as e:
               logger.error(e)

#       'rpk' is our signal that this is a non-Causal filter with zero phase
#       inserted into fil dictionary after fil_save and convert
        # sig_tx -> select_filter -> filter_specs   
        self.sig_tx.emit({'sender':__name__, 'filt_changed':'ellip_zero'})

#------------------------------------------------------------------------------
#
#         DESIGN ROUTINES
#
#------------------------------------------------------------------------------

    # LP: F_PB < F_stop -------------------------------------------------------
    def LPmin(self, fil_dict):
        """Elliptic LP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB,                                                        analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1              
        #logger.warning("and "+str(self.F_PBC) + " " + str(self.N))
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                            btype='low', analog=self.analog, output=self.FRMT))

    def LPman(self, fil_dict):
        """Elliptic LP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB,
                            btype='low', analog=self.analog, output=self.FRMT))

    # HP: F_stop < F_PB -------------------------------------------------------
    def HPmin(self, fil_dict):
        """Elliptic HP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB,
                                                          analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='highpass', analog=self.analog, output=self.FRMT))

    def HPman(self, fil_dict):
        """Elliptic HP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB,
                        btype='highpass', analog=self.analog, output=self.FRMT))

    # For BP and BS, F_XX have two elements each, A_XX has only one

    # BP: F_SB[0] < F_PB[0], F_SB[1] > F_PB[1] --------------------------------
    def BPmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
            [self.F_SB, self.F_SB2], self.A_PB, self.A_SB, analog=self.analog)
        #logger.warning(" "+str(self.F_PBC) + " " + str(self.N))
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        #logger.warning("-"+str(self.F_PBC) + " " + str(self.A_SB))
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='bandpass', analog=self.analog, output=self.FRMT))

    def BPman(self, fil_dict):
        """Elliptic BP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB,
            [self.F_PB,self.F_PB2], btype='bandpass', analog=self.analog,
                                                            output=self.FRMT))

    # BS: F_SB[0] > F_PB[0], F_SB[1] < F_PB[1] --------------------------------
    def BSmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                [self.F_SB, self.F_SB2], self.A_PB,self.A_SB,                                                       analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='bandstop', analog=self.analog, output=self.FRMT))

    def BSman(self, fil_dict):
        """Elliptic BS filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB,
            [self.F_PB,self.F_PB2], btype='bandstop', analog=self.analog,
                                                            output=self.FRMT))
示例#7
0
class AllpPZ(QWidget):

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

    info = """
**Allpass widget**

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

    """

    sig_tx = pyqtSignal(object)

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

        self.p = [0.5, 0.5j]

        self.ft = 'IIR'

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

        self.info_doc = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self._save(fil_dict, zpk_list)
示例#8
0
class Plot_Tran_Stim_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming:
    sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)
    # outgoing: to fft related widgets (FFT window widget, qfft_win_select)
    sig_tx_fft = pyqtSignal(object)

    from pyfda.libs.pyfda_qt_lib import emit

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

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

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

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

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

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

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

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

        self.scale_impz = 1  # optional energy scaling for impulses

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        layGStim = QGridLayout()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self._update_scale_impz()

        self.update_freqs()

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

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

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

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

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

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

        """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def _update_stim_par1(self):
        """ Update value for self.par1 from QLineEditWidget"""
        self.stim_par1 = safe_eval(self.ledStimPar1.text(),
                                   self.stim_par1,
                                   sign='pos',
                                   return_type='float')
        self.ledStimPar1.setText(str(self.stim_par1))
        self.emit({'ui_changed': 'stim_par1'})
示例#9
0
class MA(QWidget):

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

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

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

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

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

**Design routines:**

``ma.calc_ma()``
    """
    sig_tx = pyqtSignal(object)
    from pyfda.libs.pyfda_qt_lib import emit

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

        self.delays = 12  # number of delays per stage
        self.stages = 1  # number of stages

        self.ft = 'FIR'

        self.rt_dicts = ()
        # Common data for all filter response types:
        # This data is merged with the entries for individual response types
        # (common data comes first):

        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "Enter desired order (= delays) <b><i>M</i></b> per stage and"
                     " the number of <b>stages</b>. Target frequencies and amplitudes"
                     " are only used for comparison, not for the design itself."
                     )
                },
                'min': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "Enter desired attenuation <b><i>A<sub>SB</sub></i></b> at "
                     "the corner of the stop band <b><i>F<sub>SB</sub></i></b>. "
                     "Choose the number of <b>stages</b>, the minimum order <b><i>M</i></b> "
                     "per stage will be determined. Passband specs are not regarded."
                     )
                }
            },
            'LP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_PB', 'F_SB'),
                        'amp': ('u', 'A_PB', 'A_SB')
                    })
                },
                'min': {
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('a', 'A_PB', 'A_SB')
                    })
                }
            },
            'HP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_SB', 'F_PB'),
                        'amp': ('u', 'A_SB', 'A_PB')
                    })
                },
                'min': {
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('a', 'A_SB', 'A_PB')
                    })
                },
            },
            'BS': {
                'man': {
                    'tspecs': ('u', {
                        'frq': ('u', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('u', 'A_PB', 'A_SB', 'A_PB2')
                    }),
                    'msg':
                    ('a', "\nThis is not a proper band stop, it only lets pass"
                     " frequency components around DC and <i>f<sub>S</sub></i>/2."
                     " The order needs to be odd."),
                }
            },
            'BP': {
                'man': {
                    'tspecs': ('u', {
                        'frq': (
                            'u',
                            'F_SB',
                            'F_PB',
                            'F_PB2',
                            'F_SB2',
                        ),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    }),
                    'msg':
                    ('a', "\nThis is not a proper band pass, it only lets pass"
                     " frequency components around <i>f<sub>S</sub></i>/4."
                     " The order needs to be odd."),
                }
            },
        }

        self.info_doc = []
#        self.info_doc.append('remez()\n=======')
#        self.info_doc.append(sig.remez.__doc__)
#        self.info_doc.append('remezord()\n==========')
#        self.info_doc.append(remezord.__doc__)

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

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

        self.lbl_delays = QLabel("<b><i>M =</ i></ b>", self)
        self.lbl_delays.setObjectName('wdg_lbl_ma_0')
        self.led_delays = QLineEdit(self)
        try:
            self.led_delays.setText(str(fb.fil[0]['N']))
        except KeyError:
            self.led_delays.setText(str(self.delays))
        self.led_delays.setObjectName('wdg_led_ma_0')
        self.led_delays.setToolTip("Set number of delays per stage")

        self.lbl_stages = QLabel("<b>Stages =</ b>", self)
        self.lbl_stages.setObjectName('wdg_lbl_ma_1')
        self.led_stages = QLineEdit(self)
        self.led_stages.setText(str(self.stages))

        self.led_stages.setObjectName('wdg_led_ma_1')
        self.led_stages.setToolTip("Set number of stages ")

        self.chk_norm = QCheckBox("Normalize", self)
        self.chk_norm.setChecked(True)
        self.chk_norm.setObjectName('wdg_chk_ma_2')
        self.chk_norm.setToolTip("Normalize to| H_max = 1|")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_delays)
        self.layHWin.addWidget(self.led_delays)
        self.layHWin.addStretch(1)
        self.layHWin.addWidget(self.lbl_stages)
        self.layHWin.addWidget(self.led_stages)
        self.layHWin.addStretch(1)
        self.layHWin.addWidget(self.chk_norm)
        self.layHWin.setContentsMargins(0, 0, 0, 0)
        # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits)
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

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

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

    def _load_dict(self):
        """
        Reload parameter(s) from filter dictionary (if they exist) and set
        corresponding UI elements. load_dict() is called upon initialization
        and when the filter is loaded from disk.
        """
        if 'wdg_fil' in fb.fil[0] and 'ma' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['ma']
            if 'delays' in wdg_fil_par:
                self.delays = wdg_fil_par['delays']
                self.led_delays.setText(str(self.delays))
            if 'stages' in wdg_fil_par:
                self.stages = wdg_fil_par['stages']
                self.led_stages.setText(str(self.stages))
            if 'normalize' in wdg_fil_par:
                self.chk_norm.setChecked(wdg_fil_par['normalize'])

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and resize the textfields according to content.
        """
        self.delays = safe_eval(self.led_delays.text(),
                                self.delays,
                                return_type='int',
                                sign='pos')
        self.led_delays.setText(str(self.delays))
        self.stages = safe_eval(self.led_stages.text(),
                                self.stages,
                                return_type='int',
                                sign='pos')
        self.led_stages.setText(str(self.stages))

        self._store_entries()

    def _store_entries(self):
        """
        Store parameter settings in filter dictionary. Called from _update_UI()
        and _save()
        """
        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update({
            'ma': {
                'delays': self.delays,
                'stages': self.stages,
                'normalize': self.chk_norm.isChecked()
            }
        })
        # sig_tx -> select_filter -> filter_specs
        self.emit({'filt_changed': 'ma'})

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        # N is total order, L is number of taps per stage
        self.F_SB = fil_dict['F_SB']
        self.A_SB = fil_dict['A_SB']

    def _save(self, fil_dict):
        """
        Save MA-filters both in 'zpk' and 'ba' format; no conversion has to be
        performed except maybe deleting an 'sos' entry from an earlier
        filter design.
        """
        if 'zpk' in self.FRMT:
            fil_save(fil_dict, self.zpk, 'zpk', __name__, convert=False)

        if 'ba' in self.FRMT:
            fil_save(fil_dict, self.b, 'ba', __name__, convert=False)

        fil_convert(fil_dict, self.FRMT)

        # always update filter dict and LineEdit, in case the design algorithm
        # has changed the number of delays:
        fil_dict['N'] = self.delays * self.stages  # updated filter order
        self.led_delays.setText(str(self.delays))  # updated number of delays

        self._store_entries()

    def calc_ma(self, fil_dict, rt='LP'):
        """
        Calculate coefficients and P/Z for moving average filter based on
        filter length L = N + 1 and number of cascaded stages and save the
        result in the filter dictionary.
        """
        b = 1.
        k = 1.
        L = self.delays + 1

        if rt == 'LP':
            b0 = np.ones(L)  #  h[n] = {1; 1; 1; ...}
            i = np.arange(1, L)

            norm = L

        elif rt == 'HP':
            b0 = np.ones(L)
            b0[::2] = -1.  # h[n] = {1; -1; 1; -1; ...}

            i = np.arange(L)
            if (L % 2 == 0):  # even order, remove middle element
                i = np.delete(i, round(L / 2.))
            else:  # odd order, shift by 0.5 and remove middle element
                i = np.delete(i, int(L / 2.)) + 0.5

            norm = L

        elif rt == 'BP':
            # N is even, L is odd
            b0 = np.ones(L)
            b0[1::2] = 0
            b0[::4] = -1  # h[n] = {1; 0; -1; 0; 1; ... }

            L = L + 1
            i = np.arange(L)  # create N + 2 zeros around the unit circle, ...
            # ... remove first and middle element and rotate by L / 4
            i = np.delete(i, [0, L // 2]) + L / 4

            norm = np.sum(abs(b0))

        elif rt == 'BS':
            # N is even, L is odd
            b0 = np.ones(L)
            b0[1::2] = 0

            L = L + 1
            i = np.arange(
                L)  # create N + 2 zeros around the unit circle and ...
            i = np.delete(i,
                          [0, L // 2])  # ... remove first and middle element

            norm = np.sum(b0)

        if self.delays > 1000:
            if not qfilter_warning(None, self.delays * self.stages,
                                   "Moving Average"):
                return -1

        z0 = np.exp(-2j * np.pi * i / L)
        # calculate filter for multiple cascaded stages
        for i in range(self.stages):
            b = np.convolve(b0, b)
        z = np.repeat(z0, self.stages)

        # normalize filter to |H_max| = 1 if checked:
        if self.chk_norm.isChecked():
            b = b / (norm**self.stages)
            k = 1. / norm**self.stages
        p = np.zeros(len(z))

        # store in class attributes for the _save method
        self.zpk = [z, p, k]
        self.b = b
        self._save(fil_dict)

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='LP')

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = int(
            np.ceil(
                1 /
                (self.A_SB**(1 / self.stages) * np.sin(self.F_SB * np.pi))))
        self.calc_ma(fil_dict, rt='LP')

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='HP')

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = int(
            np.ceil(1 / (self.A_SB**(1 / self.stages) * np.sin(
                (0.5 - self.F_SB) * np.pi))))
        self.calc_ma(fil_dict, rt='HP')

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = ceil_odd(self.delays)  # enforce odd order
        self.calc_ma(fil_dict, rt='BS')

    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        self.delays = ceil_odd(self.delays)  # enforce odd order
        self.calc_ma(fil_dict, rt='BP')
示例#10
0
class Equiripple(QWidget):

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

    info = """
**Equiripple filters**

have the steepest rate of transition between the frequency response’s passband
and stopband of all FIR filters. This comes at the expense of a constant ripple
(equiripple) :math:`A_PB` and :math:`A_SB` in both pass and stop band.

The filter-coefficients are calculated in such a way that the transfer function
minimizes the maximum error (**Minimax** design) between the desired gain and the
realized gain in the specified frequency bands using the **Remez** exchange algorithm.
The filter design algorithm is known as **Parks-McClellan** algorithm, in
Matlab (R) it is called ``firpm``.

Manual filter order design requires specifying the frequency bands (:math:`F_PB`,
:math:`f_SB` etc.), the filter order :math:`N` and weight factors :math:`W_PB`,
:math:`W_SB` etc.) for individual bands.

The minimum order and the weight factors needed to fulfill the target specifications
is estimated from frequency and amplitude specifications using Ichige's algorithm.

**Design routines:**

``scipy.signal.remez()``, ``pyfda_lib.remezord()``
    """

    sig_tx = pyqtSignal(object)

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

        self.grid_density = 16

        self.ft = 'FIR'

        self.rt_dicts = ('com', )

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

        self.info_doc = []
        self.info_doc.append('remez()\n=======')
        self.info_doc.append(sig.remez.__doc__)
        self.info_doc.append('remezord()\n==========')
        self.info_doc.append(remezord.__doc__)

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in 
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        self.lbl_remez_1 = QLabel("Grid Density", self)
        self.lbl_remez_1.setObjectName('wdg_lbl_remez_1')
        self.led_remez_1 = QLineEdit(self)
        self.led_remez_1.setText(str(self.grid_density))
        self.led_remez_1.setObjectName('wdg_led_remez_1')
        self.led_remez_1.setToolTip(
            "Number of frequency points for Remez algorithm. Increase the\n"
            "number to reduce frequency overshoot in the transition region.")

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

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

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

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.grid_density = safe_eval(self.led_remez_1.text(),
                                      self.grid_density,
                                      return_type='int',
                                      sign='pos')
        self.led_remez_1.setText(str(self.grid_density))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'equiripple': {
                'grid_density': self.grid_density
            }})

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

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

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N = fil_dict['N'] + 1  # remez algorithms expects number of taps
        # which is larger by one than the order!!
        self.F_PB = fil_dict['F_PB']
        self.F_SB = fil_dict['F_SB']
        self.F_PB2 = fil_dict['F_PB2']
        self.F_SB2 = fil_dict['F_SB2']
        # remez amplitude specs are linear (not in dBs)
        self.A_PB = fil_dict['A_PB']
        self.A_PB2 = fil_dict['A_PB2']
        self.A_SB = fil_dict['A_SB']
        self.A_SB2 = fil_dict['A_SB2']

        self.alg = 'ichige'

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

    def _save(self, fil_dict, arg):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        try:
            fil_save(fil_dict, arg, self.FRMT, __name__)
        except Exception as e:
            # catch exception due to malformatted coefficients:
            logger.error("While saving the equiripple filter design, "
                         "the following error occurred:\n{0}".format(e))
            return -1

        if str(fil_dict['fo']) == 'min':
            fil_dict['N'] = self.N - 1  # yes, update filterbroker

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB, self.F_SB, 0.5], [1, 0],
                      weight=[fil_dict['W_PB'], fil_dict['W_SB']],
                      fs=1,
                      grid_density=self.grid_density))

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_PB, self.F_SB], [1, 0],
                                     [self.A_PB, self.A_SB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        if (self.N % 2 == 0):  # even order, use odd symmetry (type III)
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:  # odd order,
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_SB, self.F_PB], [0, 1],
                                     [self.A_SB, self.A_PB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
#        self.N = ceil_odd(N)  # enforce odd order
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        if (self.N % 2 == 0):  # even order
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    # For BP and BS, F_PB and F_SB have two elements each
    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                grid_density=self.grid_density))

    def BPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A,
         W) = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2],
                       [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2],
                       fs=1,
                       alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        fil_dict['W_SB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [0, 1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = round_odd(self.N)  # enforce odd order
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_PB, self.F_SB, self.F_SB2, self.F_PB2, 0.5],
                [1, 0, 1],
                weight=[fil_dict['W_PB'], fil_dict['W_SB'], fil_dict['W_PB2']],
                fs=1,
                grid_density=self.grid_density))

    def BSmin(self, fil_dict):
        self._get_params(fil_dict)
        (N, F, A, W) = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2],
                                [1, 0, 1], [self.A_PB, self.A_SB, self.A_PB2],
                                fs=1,
                                alg=self.alg)
        self.N = round_odd(N)  # enforce odd order
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        fil_dict['W_PB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0, 1],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HILman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                type='hilbert',
                grid_density=self.grid_density))

    def DIFFman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = ceil_even(self.N)  # enforce even order
        if self.F_PB < 0.1:
            logger.warning(
                "Bandwidth for pass band ({0}) is too low, inreasing to 0.1".
                format(self.F_PB))
            self.F_PB = 0.1
            fil_dict['F_PB'] = self.F_PB
            self.sig_tx.emit({
                'sender': __name__,
                'specs_changed': 'equiripple'
            })

        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB], [np.pi * fil_dict['W_PB']],
                      fs=1,
                      type='differentiator',
                      grid_density=self.grid_density))
示例#11
0
class EllipZeroPhz(QWidget):

    #    Since we are also using poles/residues -> let's force zpk
    FRMT = 'zpk'

    info = """
**Elliptic filters with zero phase**

(also known as Cauer filters) have the steepest rate of transition between the 
frequency response’s passband and stopband of all IIR filters. This comes
at the expense of a constant ripple (equiripple) :math:`A_PB` and :math:`A_SB`
in both pass and stop band. Ringing of the step response is increased in
comparison to Chebychev filters.
 
As the passband ripple :math:`A_PB` approaches 0, the elliptical filter becomes
a Chebyshev type II filter. As the stopband ripple :math:`A_SB` approaches 0,
it becomes a Chebyshev type I filter. As both approach 0, becomes a Butterworth
filter (butter).

For the filter design, the order :math:`N`, minimum stopband attenuation
:math:`A_SB` and the critical frequency / frequencies :math:`F_PB` where the 
gain first drops below the maximum passband ripple :math:`-A_PB` have to be specified.

The ``ellipord()`` helper routine calculates the minimum order :math:`N` and 
critical passband frequency :math:`F_C` from pass and stop band specifications.

The Zero Phase Elliptic Filter squares an elliptic filter designed in
a way to produce the required Amplitude specifications. So initially the
amplitude specs design an elliptic filter with the square root of the amp specs.
The filter is then squared to produce a zero phase filter.
The filter coefficients are applied to the signal data in a backward and forward
time fashion.  This filter can only be applied to stored signal data (not
real-time streaming data that comes in a forward time order).

We are forcing the order N of the filter to be even.  This simplifies the poles/zeros
to be complex (no real values).

**Design routines:**

``scipy.signal.ellip()``, ``scipy.signal.ellipord()``

        """
    sig_tx = pyqtSignal(object)

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

        self.ft = 'IIR'

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

        self.rt_dict_add = {
            'COM': {
                'man': {
                    'msg':
                    ('a',
                     "Enter the filter order <b><i>N</i></b>, the minimum stop "
                     "band attenuation <b><i>A<sub>SB</sub></i></b> and frequency or "
                     "frequencies <b><i>F<sub>C</sub></i></b>  where gain first drops "
                     "below the max passband ripple <b><i>-A<sub>PB</sub></i></b> ."
                     )
                }
            },
            'LP': {
                'man': {},
                'min': {}
            },
            'HP': {
                'man': {},
                'min': {}
            },
            'BS': {
                'man': {},
                'min': {}
            },
            'BP': {
                'man': {},
                'min': {}
            },
        }

        self.info_doc = []
        self.info_doc.append('ellip()\n========')
        self.info_doc.append(sig.ellip.__doc__)
        self.info_doc.append('ellipord()\n==========')
        self.info_doc.append(ellipord.__doc__)

    #--------------------------------------------------------------------------
    def construct_UI(self):
        """
        Create additional subwidget(s) needed for filter design:
        These subwidgets are instantiated dynamically when needed in
        select_filter.py using the handle to the filter instance, fb.fil_inst.
        """
        # =============================================================================
        #         self.chkComplex   = QCheckBox("ComplexFilter", self)
        #         self.chkComplex.setToolTip("Designs BP or BS Filter for complex data.")
        #         self.chkComplex.setObjectName('wdg_lbl_el')
        #         self.chkComplex.setChecked(False)
        #
        # =============================================================================
        self.butSave = QPushButton(self)
        self.butSave.setText("SAVE")
        self.butSave.setToolTip("Save filter in proprietary format")

        #--------------------------------------------------
        #  Layout for filter optional subwidgets
        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        #self.layHWin.addWidget(self.chkComplex)
        self.layHWin.addWidget(self.butSave)
        self.layHWin.addStretch()
        self.layHWin.setContentsMargins(0, 0, 0, 0)

        # Widget containing all subwidgets
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        self.butSave.clicked.connect(self.save_filter)

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        For zero phase filter, we take square root of amplitude specs
        since we later square filter.  Define design around smallest amp spec
        """
        # Frequencies are normalized to f_Nyq = f_S/2, ripple specs are in dB
        self.analog = False  # set to True for analog filters
        self.manual = False  # default is normal design
        self.N = int(fil_dict['N'])

        # force N to be even
        if (self.N % 2) == 1:
            self.N += 1
        self.F_PB = fil_dict['F_PB'] * 2
        self.F_SB = fil_dict['F_SB'] * 2
        self.F_PB2 = fil_dict['F_PB2'] * 2
        self.F_SB2 = fil_dict['F_SB2'] * 2
        self.F_PBC = None

        # find smallest spec'd linear value and rewrite dictionary
        ampPB = fil_dict['A_PB']
        ampSB = fil_dict['A_SB']

        # take square roots of amp specs so resulting squared
        # filter will meet specifications
        if (ampPB < ampSB):
            ampSB = sqrt(ampPB)
            ampPB = sqrt(1 + ampPB) - 1
        else:
            ampPB = sqrt(1 + ampSB) - 1
            ampSB = sqrt(ampSB)
        self.A_PB = lin2unit(ampPB, 'IIR', 'A_PB', unit='dB')
        self.A_SB = lin2unit(ampSB, 'IIR', 'A_SB', unit='dB')
        #logger.warning("design with "+str(self.A_PB)+","+str(self.A_SB))

        # ellip filter routines support only one amplitude spec for
        # pass- and stop band each
        if str(fil_dict['rt']) == 'BS':
            fil_dict['A_PB2'] = self.A_PB
        elif str(fil_dict['rt']) == 'BP':
            fil_dict['A_SB2'] = self.A_SB

#   partial fraction expansion to define residue vector

    def _partial(self, k, p, z, norder):
        # create diff array
        diff = p - z

        # now compute residual vector
        cone = complex(1., 0.)
        residues = zeros(norder, complex)
        for i in range(norder):
            residues[i] = k * (diff[i] / p[i])
            for j in range(norder):
                if (j != i):
                    residues[i] = residues[i] * (cone + diff[j] /
                                                 (p[i] - p[j]))

        # now compute DC term for new expansion
        sumRes = 0.
        for i in range(norder):
            sumRes = sumRes + residues[i].real

        dc = k - sumRes

        return (dc, residues)

#
# Take a causal filter and square it. The result has the square
#  of the amplitude response of the input, and zero phase. Filter
#  is noncausal.
# Input:
#   k - gain in pole/zero form
#   p - numpy array of poles
#   z - numpy array of zeros
#   g - gain in pole/residue form
#   r - numpy array of residues
#   nn- order of filter

# Output:
#   kn - new gain (pole/zero)
#   pn - new poles
#   zn - new zeros  (numpy array)
#   gn - new gain (pole/residue)
#   rn - new residues

    def _sqCausal(self, k, p, z, g, r, nn):

        #       Anticausal poles have conjugate-reciprocal symmetry
        #       Starting anticausal residues are conjugates (adjusted below)

        pA = conj(1. / p)  # antiCausal poles
        zA = conj(z)  # antiCausal zeros (store reciprocal)
        rA = conj(r)  # antiCausal residues (to start)
        rC = zeros(nn, complex)

        #       Adjust residues. Causal part first.
        for j in range(nn):

            #           Evaluate the anticausal filter at each causal pole
            tmpx = rA / (1. - p[j] / pA)
            ztmp = g + sum(tmpx)

            #           Adjust residue
            rC[j] = r[j] * ztmp

#       anticausal residues are just conjugates of causal residues
#        r3 = np.conj(r2)

#       Compute the constant term
        dc2 = (g + sum(r)) * g - sum(rC)

        #       Populate output (2nn elements)
        gn = dc2.real

        #       Drop complex poles/residues in LHP, keep only UHP

        pA = conj(p)  #store AntiCasual pole (reciprocal)
        p0 = zeros(int(nn / 2), complex)
        r0 = zeros(int(nn / 2), complex)
        cnt = 0
        for j in range(nn):
            if (p[j].imag > 0.0):
                p0[cnt] = p[j]
                r0[cnt] = rC[j]
                cnt = cnt + 1

#       Let operator know we squared filter
#        logger.info('After squaring filter, order: '+str(nn*2))

#       For now and our case, only store causal residues
#       Filters are symmetric and can generate antiCausal residues
        return (pA, zA, gn, p0, r0)

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

#   custom save of filter dictionary

    def _save(self, fil_dict, arg):
        """
        First design initial elliptic filter meeting sqRoot Amp specs;
         - Then create residue vector from poles/zeros;
         - Then square filter (k,p,z and dc,p,r) to get zero phase filter;
         - Then Convert results of filter design to all available formats (pz, pr, ba, sos)
        and store them in the global filter dictionary.

        Corner frequencies and order calculated for minimum filter order are
        also stored to allow for an easy subsequent manual filter optimization.
        """
        fil_save(fil_dict, arg, self.FRMT, __name__)

        # For min. filter order algorithms, update filter dict with calculated
        # new values for filter order N and corner frequency(s) F_PBC

        fil_dict['N'] = self.N
        if str(fil_dict['fo']) == 'min':
            if str(fil_dict['rt']) == 'LP' or str(fil_dict['rt']) == 'HP':
                #               HP or LP - single  corner frequency
                fil_dict['F_PB'] = self.F_PBC / 2.
            else:  # BP or BS - two corner frequencies
                fil_dict['F_PB'] = self.F_PBC[0] / 2.
                fil_dict['F_PB2'] = self.F_PBC[1] / 2.

#       Now generate poles/residues for custom file save of new parameters
        if (not self.manual):
            z = fil_dict['zpk'][0]
            p = fil_dict['zpk'][1]
            k = fil_dict['zpk'][2]
            n = len(z)
            gain, residues = self._partial(k, p, z, n)

            pA, zA, gn, pC, rC = self._sqCausal(k, p, z, gain, residues, n)
            fil_dict['rpk'] = [rC, pC, gn]

            #           save antiCausal b,a (nonReciprocal) also [easier to compute h(n)
            try:
                fil_dict['baA'] = sig.zpk2tf(zA, pA, k)
            except Exception as e:
                logger.error(e)

#       'rpk' is our signal that this is a non-Causal filter with zero phase
#       inserted into fil dictionary after fil_save and convert
# sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'ellip_zero'})

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

    def save_filter(self):
        file_filters = ("Text file pole/residue (*.txt_rpk)")
        dlg = QFD(self)
        # return selected file name (with or without extension) and filter (Linux: full text)
        file_name, file_type = dlg.getSaveFileName_(caption="Save filter as",
                                                    directory=dirs.save_dir,
                                                    filter=file_filters)

        file_name = str(file_name)  # QString -> str() needed for Python 2.x
        # Qt5 has QFileDialog.mimeTypeFilters(), but under Qt4 the mime type cannot
        # be extracted reproducibly across file systems, so it is done manually:

        for t in extract_file_ext(
                file_filters):  # get a list of file extensions
            if t in str(file_type):
                file_type = t  # return the last matching extension

        if file_name != "":  # cancelled file operation returns empty string

            # strip extension from returned file name (if any) + append file type:
            file_name = os.path.splitext(file_name)[0] + file_type

            file_type_err = False
            try:

                # save as a custom residue/pole text output for apply with custom tool
                # make sure we have the residues
                if 'rpk' in fb.fil[0]:
                    with io.open(file_name, 'w', encoding="utf8") as f:
                        self.file_dump(f)
                else:
                    file_type_err = True
                    logger.error(
                        'Filter has no residues/poles, cannot save as *.txt_rpk file'
                    )
                if not file_type_err:
                    logger.info('Successfully saved filter as\n\t"{0}"'.format(
                        file_name))
                    dirs.save_dir = os.path.dirname(file_name)  # save new dir

            except IOError as e:
                logger.error('Failed saving "{0}"!\n{1}'.format(file_name, e))

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

    def file_dump(self, fOut):
        """
        Dump file out in custom text format that apply tool can read to know filter coef's
        """

        #       Fixed format widths for integers and doubles
        intw = '10'
        dblW = 27
        frcW = 20

        #       Fill up character string with filter output
        filtStr = '# IIR filter\n'

        #       parameters that made filter (choose smallest eps)
        #       Amp is stored in Volts (linear units)
        #       the second amp terms aren't really used (for ellip filters)

        FA_PB = fb.fil[0]['A_PB']
        FA_SB = fb.fil[0]['A_SB']
        FAmp = min(FA_PB, FA_SB)

        #       Freq terms in radians so move from -1:1 to -pi:pi
        f_lim = fb.fil[0]['freqSpecsRange']
        f_unit = fb.fil[0]['freq_specs_unit']

        F_S = fb.fil[0]['f_S']
        if fb.fil[0]['freq_specs_unit'] == 'f_S':
            F_S = F_S * 2
        F_SB = fb.fil[0]['F_SB'] * F_S * np.pi
        F_SB2 = fb.fil[0]['F_SB2'] * F_S * np.pi
        F_PB = fb.fil[0]['F_PB'] * F_S * np.pi
        F_PB2 = fb.fil[0]['F_PB2'] * F_S * np.pi

        #       Determine pass/stop bands depending on filter response type
        passMin = []
        passMax = []
        stopMin = []
        stopMax = []

        if fb.fil[0]['rt'] == 'LP':
            passMin = [-F_PB, 0, 0]
            passMax = [F_PB, 0, 0]
            stopMin = [-np.pi, F_SB, 0]
            stopMax = [-F_SB, np.pi, 0]
            f1 = F_PB
            f2 = F_SB
            f3 = f4 = 0
            Ftype = 1
            Fname = 'Low_Pass'

        if fb.fil[0]['rt'] == 'HP':
            passMin = [-np.pi, F_PB, 0]
            passMax = [-F_PB, np.pi, 0]
            stopMin = [-F_SB, 0, 0]
            stopMax = [F_SB, 0, 0]
            f1 = F_SB
            f2 = F_PB
            f3 = f4 = 0
            Ftype = 2
            Fname = 'Hi_Pass'

        if fb.fil[0]['rt'] == 'BS':
            passMin = [-np.pi, -F_PB, F_PB2]
            passMax = [-F_PB2, F_PB, np.pi]
            stopMin = [-F_SB2, F_SB, 0]
            stopMax = [-F_SB, F_SB2, 0]
            f1 = F_PB
            f2 = F_SB
            f3 = F_SB2
            f4 = F_PB2
            Ftype = 4
            Fname = 'Band_Stop'

        if fb.fil[0]['rt'] == 'BP':
            passMin = [-F_PB2, F_PB, 0]
            passMax = [-F_PB, F_PB2, 0]
            stopMin = [-np.pi, -F_SB, F_SB2]
            stopMax = [-F_SB2, F_SB, np.pi]
            f1 = F_SB
            f2 = F_PB
            f3 = F_PB2
            f4 = F_SB2
            Ftype = 3
            Fname = 'Band_Pass'

        filtStr = filtStr + '{:{align}{width}}'.format(
            '10', align='>', width=intw) + ' IIRFILT_4SYM\n'
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(Ftype), align='>', width=intw) + ' ' + Fname + '\n'
        filtStr = filtStr + '{:{d}.{p}f}'.format(FAmp, d=dblW, p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[0], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[0], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[1], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[1], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[2], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[2], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[0], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[0], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[1], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[1], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[2], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[2], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(f1, d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(f2, d=dblW, p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(f3, d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(f4, d=dblW, p=frcW) + '\n'

        #       move pol/res/gain into terms we need
        Fdc = fb.fil[0]['rpk'][2]
        rC = fb.fil[0]['rpk'][0]
        pC = fb.fil[0]['rpk'][1]
        Fnum = len(pC)

        #       Gain term
        filtStr = filtStr + '{: {d}.{p}e}'.format(Fdc, d=dblW, p=frcW) + '\n'

        #       Real pole count inside the unit circle (none of these)

        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Complex pole/res count inside the unit circle

        filtStr = filtStr + '{:{i}d}'.format(Fnum, i=intw) + '\n'

        #       Now dump poles/residues
        for j in range(Fnum):
            filtStr = filtStr + '{:{i}d}'.format(j, i=intw) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                rC[j].real, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                rC[j].imag, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                pC[j].real, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                pC[j].imag, d=dblW, p=frcW) + '\n'

#       Real pole count outside the unit circle (none of these)
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Complex pole count outside the unit circle (none of these)
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Now write huge text string to file
        fOut.write(filtStr)

#------------------------------------------------------------------------------
#
#         DESIGN ROUTINES
#
#------------------------------------------------------------------------------

# LP: F_PB < F_stop -------------------------------------------------------

    def LPmin(self, fil_dict):
        """Elliptic LP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,
                                      self.F_SB,
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        #logger.warning("and "+str(self.F_PBC) + " " + str(self.N))
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='low',
                      analog=self.analog,
                      output=self.FRMT))

    def LPman(self, fil_dict):
        """Elliptic LP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PB,
                      btype='low',
                      analog=self.analog,
                      output=self.FRMT))

    # HP: F_stop < F_PB -------------------------------------------------------
    def HPmin(self, fil_dict):
        """Elliptic HP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,
                                      self.F_SB,
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='highpass',
                      analog=self.analog,
                      output=self.FRMT))

    def HPman(self, fil_dict):
        """Elliptic HP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PB,
                      btype='highpass',
                      analog=self.analog,
                      output=self.FRMT))

    # For BP and BS, F_XX have two elements each, A_XX has only one

    # BP: F_SB[0] < F_PB[0], F_SB[1] > F_PB[1] --------------------------------
    def BPmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                      [self.F_SB, self.F_SB2],
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #logger.warning(" "+str(self.F_PBC) + " " + str(self.N))
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        #logger.warning("-"+str(self.F_PBC) + " " + str(self.A_SB))
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='bandpass',
                      analog=self.analog,
                      output=self.FRMT))

    def BPman(self, fil_dict):
        """Elliptic BP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB, [self.F_PB, self.F_PB2],
                      btype='bandpass',
                      analog=self.analog,
                      output=self.FRMT))

    # BS: F_SB[0] > F_PB[0], F_SB[1] < F_PB[1] --------------------------------
    def BSmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                      [self.F_SB, self.F_SB2],
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='bandstop',
                      analog=self.analog,
                      output=self.FRMT))

    def BSman(self, fil_dict):
        """Elliptic BS filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB, [self.F_PB, self.F_PB2],
                      btype='bandstop',
                      analog=self.analog,
                      output=self.FRMT))
示例#12
0
class Tran_IO_UI(QWidget):
    """
    Create the UI for the PlotImpz class
    """
    # incoming:
    sig_rx = pyqtSignal(object)
    # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx')
    sig_tx = pyqtSignal(object)

    from pyfda.libs.pyfda_qt_lib import emit

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

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

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

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

    def __init__(self, parent=None):
        """
        Pass instance `parent` of parent class (FilterCoeffs)
        """
        super(Tran_IO_UI, self).__init__(parent)
        self._construct_UI()

    def _construct_UI(self):
        # =====================================================================
        # Controls
        # =====================================================================

        self.butLoad = PushButton(self,
                                  icon=QIcon(':/file.svg'),
                                  checkable=False)
        # self.butLoad.setIconSize(q_icon_size)
        self.butLoad.setToolTip("Load data from file.")
        self.butLoad.setEnabled(False)

        self.lbl_info = QLabel(to_html("  coming soon ...", frmt="b"))

        # ----------------------------------------------------------------------
        # Main Widget
        # ----------------------------------------------------------------------
        layH_io_par = QHBoxLayout()
        layH_io_par.addWidget(self.butLoad)
        layH_io_par.addWidget(self.lbl_info)

        layV_io = QVBoxLayout()
        layV_io.addLayout(layH_io_par)

        layH_io = QHBoxLayout()
        layH_io.addLayout(layV_io)
        layH_io.addStretch(10)

        self.wdg_top = QWidget(self)
        self.wdg_top.setLayout(layH_io)
        self.wdg_top.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)