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 ± π"), ("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("Δ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>Δ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(" 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(" 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(" 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(" 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(" 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(" 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(" 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(" 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(" 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
class Input_Coeffs_UI(QWidget): """ Create the UI for the FilterCoeffs class """ sig_rx = pyqtSignal(dict) # incoming sig_tx = pyqtSignal(dict) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(Input_Coeffs_UI, self).__init__(parent) self.eps = 1.e-6 # initialize tolerance value self._construct_UI() # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the CSV pop-up window """ # logger.debug("PROCESS_SIG_RX:\n{0}".format(pprint_log(dict_sig))) if 'closeEvent' in dict_sig: self._close_csv_win() self.emit({'ui_changed': 'csv'}) return elif 'ui_changed' in dict_sig: self._set_load_save_icons() # update icons file <-> clipboard # inform e.g. the p/z input widget about changes in CSV options self.emit({'ui_changed': 'csv'}) # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - top chkbox row - coefficient table - two bottom rows with action buttons """ self.bfont = QFont() self.bfont.setBold(True) self.bifont = QFont() self.bifont.setBold(True) self.bifont.setItalic(True) # q_icon_size = QSize(20, 20) # optional, size is derived from butEnable ####################################################################### # frmMain # # This frame contains all the buttons ####################################################################### # --------------------------------------------- # layHDisplay # # UI Elements for controlling the display # --------------------------------------------- self.butEnable = PushButton(self, icon=QIcon(':/circle-check.svg'), checked=True) q_icon_size = self.butEnable.iconSize() # <- uncomment this for manual sizing self.butEnable.setToolTip( "<span>Show / hide filter coefficients in an editable table." " For high order systems, table display might be slow.</span>") fix_formats = ['Dec', 'Hex', 'Bin', 'CSD'] self.cmbFormat = QComboBox(self) model = self.cmbFormat.model() item = QtGui.QStandardItem('Float') item.setData('child', Qt.AccessibleDescriptionRole) model.appendRow(item) item = QtGui.QStandardItem('Fixp.:') item.setData('parent', Qt.AccessibleDescriptionRole) item.setData(0, QtGui.QFont.Bold) item.setFlags(item.flags() & ~Qt.ItemIsEnabled) # | Qt.ItemIsSelectable)) model.appendRow(item) for idx in range(len(fix_formats)): item = QtGui.QStandardItem(fix_formats[idx]) # item.setForeground(QtGui.QColor('red')) model.appendRow(item) self.cmbFormat.insertSeparator(1) qset_cmb_box(self.cmbFormat, 'float') self.cmbFormat.setToolTip('Set the display format.') self.cmbFormat.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.spnDigits = QSpinBox(self) self.spnDigits.setRange(0, 16) self.spnDigits.setValue(params['FMT_ba']) self.spnDigits.setToolTip("Number of digits to display.") self.lblDigits = QLabel("Digits", self) self.lblDigits.setFont(self.bifont) self.cmbQFrmt = QComboBox(self) q_formats = [('Norm. Frac.', 'qnfrac'), ('Integer', 'qint'), ('Fractional', 'qfrac')] for q in q_formats: self.cmbQFrmt.addItem(*q) self.lbl_W = QLabel("W = ", self) self.lbl_W.setFont(self.bifont) self.ledW = QLineEdit(self) self.ledW.setToolTip("Specify total wordlength.") self.ledW.setText("16") self.ledW.setMaxLength(2) # maximum of 2 digits self.ledW.setFixedWidth(30) # width of lineedit in points(?) layHDisplay = QHBoxLayout() layHDisplay.setAlignment(Qt.AlignLeft) layHDisplay.addWidget(self.butEnable) layHDisplay.addWidget(self.cmbFormat) layHDisplay.addWidget(self.spnDigits) layHDisplay.addWidget(self.lblDigits) layHDisplay.addWidget(self.cmbQFrmt) layHDisplay.addWidget(self.lbl_W) layHDisplay.addWidget(self.ledW) layHDisplay.addStretch() ####################################################################### # frmButtonsCoeffs # # This frame contains all buttons for manipulating coefficients ####################################################################### # ----------------------------------------------------------------- # layHButtonsCoeffs1 # # UI Elements for loading / storing / manipulating cells and rows # ----------------------------------------------------------------- self.cmbFilterType = QComboBox(self) self.cmbFilterType.setObjectName("comboFilterType") self.cmbFilterType.setToolTip( "<span>Select between IIR and FIR filter for manual entry." "Changing the type reloads the filter from the filter dict.</span>") self.cmbFilterType.addItems(["FIR", "IIR"]) self.cmbFilterType.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butAddCells = QPushButton(self) self.butAddCells.setIcon(QIcon(':/row_insert_above.svg')) self.butAddCells.setIconSize(q_icon_size) self.butAddCells.setToolTip( "<span>Select cells to insert a new cell above each selected cell. " "Use <SHIFT> or <CTRL> to select multiple cells. " "When nothing is selected, add a row at the end.</span>") self.butDelCells = QPushButton(self) self.butDelCells.setIcon(QIcon(':/row_delete.svg')) self.butDelCells.setIconSize(q_icon_size) self.butDelCells.setToolTip( "<span>Delete selected cell(s) from the table. " "Use <SHIFT> or <CTRL> to select multiple cells. " "When nothing is selected, delete the last row.</span>") self.butSave = QPushButton(self) self.butSave.setIcon(QIcon(':/upload.svg')) self.butSave.setIconSize(q_icon_size) self.butSave.setToolTip( "<span>Copy coefficient table to filter dict and update all plots" "and widgets.</span>") self.butLoad = QPushButton(self) self.butLoad.setIcon(QIcon(':/download.svg')) self.butLoad.setIconSize(q_icon_size) self.butLoad.setToolTip("Reload coefficient table from filter dict.") self.butClear = QPushButton(self) self.butClear.setIcon(QIcon(':/trash.svg')) self.butClear.setIconSize(q_icon_size) self.butClear.setToolTip("Clear all table entries.") self.butFromTable = QPushButton(self) self.butFromTable.setIconSize(q_icon_size) self.butToTable = QPushButton(self) self.butToTable.setIconSize(q_icon_size) self.but_csv_options = PushButton(self, icon=QIcon(':/settings.svg'), checked=False) self.but_csv_options.setIconSize(q_icon_size) self.but_csv_options.setToolTip( "<span>Select CSV format and whether " "to copy to/from clipboard or file.</span>") self._set_load_save_icons() # initialize icon / button settings layHButtonsCoeffs1 = QHBoxLayout() layHButtonsCoeffs1.addWidget(self.cmbFilterType) layHButtonsCoeffs1.addWidget(self.butAddCells) layHButtonsCoeffs1.addWidget(self.butDelCells) layHButtonsCoeffs1.addWidget(self.butClear) layHButtonsCoeffs1.addWidget(self.butSave) layHButtonsCoeffs1.addWidget(self.butLoad) layHButtonsCoeffs1.addWidget(self.butFromTable) layHButtonsCoeffs1.addWidget(self.butToTable) layHButtonsCoeffs1.addWidget(self.but_csv_options) layHButtonsCoeffs1.addStretch() # ---------------------------------------------------------------------- # layHButtonsCoeffs2 # # Eps / set zero settings # --------------------------------------------------------------------- self.butSetZero = QPushButton("= 0", self) self.butSetZero.setToolTip( "<span>Set selected coefficients = 0 with a magnitude < ε. " "When nothing is selected, test the whole table.</span>") self.butSetZero.setIconSize(q_icon_size) lblEps = QLabel(self) lblEps.setText("<b><i>for b, a</i> <</b>") self.ledEps = QLineEdit(self) self.ledEps.setToolTip("Specify tolerance value.") layHButtonsCoeffs2 = QHBoxLayout() layHButtonsCoeffs2.addWidget(self.butSetZero) layHButtonsCoeffs2.addWidget(lblEps) layHButtonsCoeffs2.addWidget(self.ledEps) layHButtonsCoeffs2.addStretch() # ------------------------------------------------------------------- # Now put the ButtonsCoeffs HBoxes into frmButtonsCoeffs # --------------------------------------------------------------------- layVButtonsCoeffs = QVBoxLayout() layVButtonsCoeffs.addLayout(layHButtonsCoeffs1) layVButtonsCoeffs.addLayout(layHButtonsCoeffs2) layVButtonsCoeffs.setContentsMargins(0, 5, 0, 0) # This frame encompasses all Quantization Settings self.frmButtonsCoeffs = QFrame(self) self.frmButtonsCoeffs.setLayout(layVButtonsCoeffs) # ###################################################################### # frmQSettings # # This frame contains all quantization settings # ###################################################################### # ------------------------------------------------------------------- # layHW_Scale # # QFormat and scale settings # --------------------------------------------------------------------- lbl_Q = QLabel("Q =", self) lbl_Q.setFont(self.bifont) self.ledWI = QLineEdit(self) self.ledWI.setToolTip("Specify number of integer bits.") self.ledWI.setText("0") self.ledWI.setMaxLength(2) # maximum of 2 digits self.ledWI.setFixedWidth(30) # width of lineedit in points(?) self.lblDot = QLabel(".", self) # class attribute, visibility is toggled self.lblDot.setFont(self.bfont) self.ledWF = QLineEdit(self) self.ledWF.setToolTip("Specify number of fractional bits.") self.ledWF.setText("15") self.ledWF.setMaxLength(2) # maximum of 2 digits # self.ledWF.setFixedWidth(30) # width of lineedit in points(?) self.ledWF.setMaximumWidth(30) self.lblScale = QLabel("<b><i>Scale</i> =</b>", self) self.ledScale = QLineEdit(self) self.ledScale.setToolTip( "Set the scale for converting float to fixpoint representation.") self.ledScale.setText(str(1)) self.ledScale.setEnabled(False) layHWI_WF = QHBoxLayout() layHWI_WF.addWidget(lbl_Q) layHWI_WF.addWidget(self.ledWI) layHWI_WF.addWidget(self.lblDot) layHWI_WF.addWidget(self.ledWF) layHWI_WF.addStretch() layHScale = QHBoxLayout() layHScale.addWidget(self.lblScale) layHScale.addWidget(self.ledScale) layHScale.addStretch() layHW_Scale = QHBoxLayout() layHW_Scale.addLayout(layHWI_WF) layHW_Scale.addLayout(layHScale) # ------------------------------------------------------------------- # layGQOpt # # Quantization / Overflow / MSB / LSB settings # --------------------------------------------------------------------- lblQOvfl = QLabel("Ovfl.:", self) lblQOvfl.setFont(self.bifont) lblQuant = QLabel("Quant.:", self) lblQuant.setFont(self.bifont) self.cmbQOvfl = QComboBox(self) qOvfl = ['wrap', 'sat'] self.cmbQOvfl.addItems(qOvfl) qset_cmb_box(self.cmbQOvfl, 'sat') self.cmbQOvfl.setToolTip("Select overflow behaviour.") # ComboBox size is adjusted automatically to fit the longest element self.cmbQOvfl.setSizeAdjustPolicy(QComboBox.AdjustToContents) layHQOvflOpt = QHBoxLayout() layHQOvflOpt.addWidget(lblQOvfl) layHQOvflOpt.addWidget(self.cmbQOvfl) layHQOvflOpt.addStretch() self.cmbQuant = QComboBox(self) qQuant = ['none', 'round', 'fix', 'floor'] self.cmbQuant.addItems(qQuant) qset_cmb_box(self.cmbQuant, 'round') self.cmbQuant.setToolTip("Select the kind of quantization.") self.cmbQuant.setSizeAdjustPolicy(QComboBox.AdjustToContents) layHQuantOpt = QHBoxLayout() layHQuantOpt.addWidget(lblQuant) layHQuantOpt.addWidget(self.cmbQuant) layHQuantOpt.addStretch() self.butQuant = QPushButton(self) self.butQuant.setToolTip( "<span>Quantize selected coefficients / " "whole table with specified settings.</span>") self.butQuant.setIcon(QIcon(':/quantize.svg')) self.butQuant.setIconSize(q_icon_size) self.butQuant.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) lblMSBtxt = QLabel(self) lblMSBtxt.setText("<b><i>MSB</i><sub>10</sub> =</b>") self.lblMSB = QLabel(self) layHMSB = QHBoxLayout() layHMSB.addWidget(lblMSBtxt) layHMSB.addWidget(self.lblMSB) layHMSB.addStretch() lblLSBtxt = QLabel(self) lblLSBtxt.setText("<b><i>LSB</i><sub>10</sub> =</b>") self.lblLSB = QLabel(self) layHLSB = QHBoxLayout() layHLSB.addWidget(lblLSBtxt) layHLSB.addWidget(self.lblLSB) layHLSB.addStretch() layGQOpt = QGridLayout() layGQOpt.addLayout(layHQOvflOpt, 0, 0) layGQOpt.addLayout(layHQuantOpt, 0, 1) layGQOpt.addWidget(self.butQuant, 0, 2, Qt.AlignCenter) layGQOpt.addLayout(layHMSB, 1, 0) layGQOpt.addLayout(layHLSB, 1, 1) # ------------------------------------------------------------------- # Display MAX # --------------------------------------------------------------------- lblMAXtxt = QLabel(self) lblMAXtxt.setText("<b><i>Max =</i></b>") self.lblMAX = QLabel(self) layHCoeffs_MAX = QHBoxLayout() layHCoeffs_MAX.addWidget(lblMAXtxt) layHCoeffs_MAX.addWidget(self.lblMAX) layHCoeffs_MAX.addStretch() ####################################################################### # Now put all the coefficient HBoxes into frmQSettings # --------------------------------------------------------------------- layVButtonsQ = QVBoxLayout() layVButtonsQ.addLayout(layHW_Scale) layVButtonsQ.addLayout(layGQOpt) layVButtonsQ.addLayout(layHCoeffs_MAX) layVButtonsQ.setContentsMargins(0, 0, 0, 0) # This frame encompasses all Quantization Settings self.frmQSettings = QFrame(self) self.frmQSettings.setLayout(layVButtonsQ) ####################################################################### # ######################## Main UI Layout ############################ ####################################################################### # layout for frame (UI widget) layVMainF = QVBoxLayout() layVMainF.addLayout(layHDisplay) layVMainF.addWidget(self.frmQSettings) layVMainF.addWidget(QHLine()) layVMainF.addWidget(self.frmButtonsCoeffs) # This frame encompasses all UI elements frmMain = QFrame(self) frmMain.setLayout(layVMainF) layVMain = QVBoxLayout() # the following affects only the first widget (intended here) layVMain.setAlignment(Qt.AlignTop) layVMain.addWidget(frmMain) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) ####################################################################### # --- set initial values from dict ------------ self.spnDigits.setValue(params['FMT_ba']) self.ledEps.setText(str(self.eps)) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_csv_options.clicked.connect(self._open_csv_win) # -------------------------------------------------------------------------- def _open_csv_win(self): """ Pop-up window for CSV options """ if self.but_csv_options.isChecked(): qstyle_widget(self.but_csv_options, "changed") else: qstyle_widget(self.but_csv_options, "normal") if dirs.csv_options_handle is None: # no handle to the window? Create a new instance if self.but_csv_options.isChecked(): # Important: Handle to window must be class attribute, otherwise it # (and the attached window) is deleted immediately when it goes # out of scope dirs.csv_options_handle = CSV_option_box(self) dirs.csv_options_handle.sig_tx.connect(self.process_sig_rx) dirs.csv_options_handle.show() # modeless i.e. non-blocking popup window else: if not self.but_csv_options.isChecked(): # this should not happen if dirs.csv_options_handle is None: logger.warning("CSV options window is already closed!") else: dirs.csv_options_handle.close() self.emit({'ui_changed': 'csv'}) # ------------------------------------------------------------------------------ def _close_csv_win(self): dirs.csv_options_handle = None self.but_csv_options.setChecked(False) qstyle_widget(self.but_csv_options, "normal") # ------------------------------------------------------------------------------ def _set_load_save_icons(self): """ Set icons / tooltipps for loading and saving data to / from file or clipboard depending on selected options. """ if params['CSV']['clipboard']: self.butFromTable.setIcon(QIcon(':/to_clipboard.svg')) self.butFromTable.setToolTip( "<span>Copy table to clipboard, SELECTED items are copied as " "displayed. When nothing is selected, the whole table " "is copied with full precision in decimal format.</span>") self.butToTable.setIcon(QIcon(':/from_clipboard.svg')) self.butToTable.setToolTip("<span>Copy clipboard to table.</span>") else: self.butFromTable.setIcon(QIcon(':/save.svg')) self.butFromTable.setToolTip( "<span>" "Save table to file, SELECTED items are copied as " "displayed. When nothing is selected, the whole table " "is copied with full precision in decimal format.</span>") self.butToTable.setIcon(QIcon(':/file.svg')) self.butToTable.setToolTip("<span>Load table from file.</span>") if dirs.csv_options_handle is None: qstyle_widget(self.but_csv_options, "normal") self.but_csv_options.setChecked(False) else: qstyle_widget(self.but_csv_options, "changed") self.but_csv_options.setChecked(True)
class Plot_Hf(QWidget): """ Widget for plotting \|H(f)\|, frequency specs and the phase """ # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self): super().__init__() self.needs_calc = True # flag whether plot needs to be updated self.needs_draw = True # flag whether plot needs to be redrawn self.tool_tip = "Magnitude and phase frequency response" self.tab_label = "|H(f)|" self.log_bottom = -80 self.lin_neg_bottom = -10 self.cmb_units_a_items = [ "<span>Set unit for y-axis</span>", ("Auto", "Auto", "Use same setting as in Ripple specifications"), ("dB", "dB", "Attenuation in dB"), ("V", "V", "Linear gain"), ("W", "W", "Power gain") ] self.cmb_units_a_default = "auto" # default setting self._construct_ui() # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ # logger.debug("SIG_RX - needs_calc = {0}, vis = {1}\n{2}"\ # .format(self.needs_calc, self.isVisible(), pprint_log(dict_sig))) if self.isVisible(): if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\ or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False self.needs_draw = False if 'view_changed' in dict_sig or self.needs_draw: self.update_view() self.needs_draw = False else: if 'data_changed' in dict_sig or 'specs_changed' in dict_sig: self.needs_calc = True if 'view_changed' in dict_sig: self.needs_draw = True def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip("Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel(to_html("Unit:", frmt="b"), self) self.cmb_units_a = QComboBox(self) qcmb_box_populate(self.cmb_units_a, self.cmb_units_a_items, self.cmb_units_a_default) self.cmb_units_a.setObjectName("cmbUnitsA") self.lbl_log_bottom = QLabel(to_html("min =", 'bi'), self) self.led_log_bottom = QLineEdit(self) self.led_log_bottom.setText(str(self.log_bottom)) self.led_log_bottom.setMaximumWidth(qtext_width(N_x=8)) self.led_log_bottom.setToolTip( "<span>Minimum display value for dB. scale.</span>") self.lbl_log_unit = QLabel("dB", self) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmb_units_a.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.but_zerophase = PushButton(" Zero phase ", checked=False) self.but_zerophase.setToolTip( "<span>Remove linear phase calculated from filter order.\n" "Attention: This makes no sense for a non-linear phase system!</span>") self.lblInset = QLabel(to_html("Inset", "bi"), self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.but_specs = PushButton("Specs ", checked=False) self.but_specs.setToolTip("Display filter specs as hatched regions") self.but_phase = PushButton("Phase ", checked=False) self.but_phase.setToolTip("Overlay phase") self.but_align = PushButton("Align", checked=True) self.but_align.setToolTip( "<span>Try to align gridlines for magnitude and phase " "(doesn't work in all cases).</span>") self.but_align.setVisible(self.but_phase.isChecked()) # ---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets # ---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmb_units_a) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom) layHControls.addWidget(self.led_log_bottom) layHControls.addWidget(self.lbl_log_unit) layHControls.addStretch(1) layHControls.addWidget(self.but_zerophase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.but_specs) layHControls.addStretch(1) layHControls.addWidget(self.but_phase) layHControls.addWidget(self.but_align) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) # ---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets # ---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_hf.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.cmb_units_a.currentIndexChanged.connect(self.draw) self.led_log_bottom.editingFinished.connect(self.update_view) self.cmbShowH.currentIndexChanged.connect(self.draw) self.but_zerophase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.but_specs.clicked.connect(self.draw) self.but_phase.clicked.connect(self.draw) self.but_align.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) # ------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes (this is run only once) """ if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes self.ax = self.mplwidget.fig.subplots() self.ax.xaxis.tick_bottom() # remove axis ticks on top self.ax.yaxis.tick_left() # remove axis ticks right # ------------------------------------------------------------------------------ def align_y_axes(self, ax1, ax2): """ Sets tick marks of twinx axes to line up with total number of ax1 tick marks """ ax1_ylims = ax1.get_ybound() # collect only visible ticks ax1_yticks = [t for t in ax1.get_yticks() if t >= ax1_ylims[0] and t <= ax1_ylims[1]] ax1_nticks = len(ax1_yticks) ax1_ydelta_lim = ax1_ylims[1] - ax1_ylims[0] # span of limits ax1_ydelta_vis = ax1_yticks[-1] - ax1_yticks[0] # delta of max. and min tick ax1_yoffset = ax1_yticks[0]-ax1_ylims[0] # offset between lower limit and first tick # calculate scale of Delta Limits / Delta Ticks ax1_scale = ax1_ydelta_lim / ax1_ydelta_vis ax2_ylims = ax2.get_ybound() ax2_yticks = ax2.get_yticks() ax2_nticks = len(ax2_yticks) #ax2_ydelta_lim = ax2_ylims[1] - ax2_ylims[0] ax2_ydelta_vis = ax2_yticks[-1] - ax2_yticks[0] ax2_ydelta_lim = ax2_ydelta_vis * ax1_scale ax2_scale = ax2_ydelta_lim / ax2_ydelta_vis # calculate new offset between lower limit and first tick ax2_yoffset = ax1_yoffset * ax2_ydelta_lim / ax1_ydelta_lim # logger.warning("ax2: delta_vis: {0}, scale: {1}, offset: {2}" # .format(ax2_ydelta_vis, ax2_scale, ax2_yoffset)) # logger.warning("Ticks: {0} # {1}".format(ax1_nticks, ax2_nticks)) ax2.set_yticks(np.linspace(ax2_yticks[0], (ax2_yticks[1]-ax2_yticks[0]), ax1_nticks)) # logger.warning("ax2[0]={0} | ax2[1]={1} ax2[-1]={2}".format(ax2_yticks[0], # ax2_yticks[1], ax2_yticks[-1])) ax2_lim0 = ax2_yticks[0] - ax2_yoffset ax2.set_ybound(ax2_lim0, ax2_lim0 + ax2_ydelta_lim) # ============================================================================= # # https://stackoverflow.com/questions/26752464/how-do-i-align-gridlines-for-two-y-axis-scales-using-matplotlib # # works, but both axes have ugly numbers # nticks = 11 # ax.yaxis.set_major_locator(ticker.LinearLocator(nticks)) # self.ax_p.yaxis.set_major_locator(ticker.LinearLocator(nticks)) # # ============================================================================= # ============================================================================= # # https://stackoverflow.com/questions/45037386/trouble-aligning-ticks-for-matplotlib-twinx-axes # # works, but second axis has ugly numbering # l_H = ax.get_ylim() # l_p = self.ax_p.get_ylim() # f = lambda x : l_p[0]+(x-l_H[0])/(l_H[1]-l_H[0])*(l_p[1]-l_p[0]) # ticks = f(ax.get_yticks()) # self.ax_p.yaxis.set_major_locator(ticker.FixedLocator(ticks)) # # ============================================================================= # http://stackoverflow.com/questions/28692608/align-grid-lines-on-two-plots # http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # http://stackoverflow.com/questions/20243683/matplotlib-align-twinx-tick-marks # manual setting: #self.ax_p.set_yticks( np.linspace(self.ax_p.get_ylim()[0],self.ax_p.get_ylim()[1],nbins) ) #ax1.set_yticks(np.linspace(ax1.get_ybound()[0], ax1.get_ybound()[1], 5)) #ax2.set_yticks(np.linspace(ax2.get_ybound()[0], ax2.get_ybound()[1], 5)) #http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # use helper functions from matplotlib.ticker: # MaxNLocator: set no more than nbins + 1 ticks #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.MaxNLocator(nbins = nbins) ) # further options: integer = False, # prune = [‘lower’ | ‘upper’ | ‘both’ | None] Remove edge ticks # AutoLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.AutoLocator() ) # LinearLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.LinearLocator(numticks = nbins -1 ) ) # self.ax_p.locator_params(axis = 'y', nbins = nbins) # # self.ax_p.set_yticks(np.linspace(self.ax_p.get_ybound()[0], # self.ax_p.get_ybound()[1], # len(self.ax.get_yticks())-1)) #N = source_ax.xaxis.get_major_ticks() #target_ax.xaxis.set_major_locator(LinearLocator(N)) #------------------------------------------------------------------------------ def plot_spec_limits(self, ax): """ Plot the specifications limits (F_SB, A_SB, ...) as hatched areas with borders. """ hatch = params['mpl_hatch'] hatch_borders = params['mpl_hatch_border'] def dB(lin): return 20 * np.log10(lin) def _plot_specs(): # upper limits: ax.plot(F_lim_upl, A_lim_upl, F_lim_upc, A_lim_upc, F_lim_upr, A_lim_upr, **hatch_borders) if A_lim_upl: ax.fill_between(F_lim_upl, max(A_lim_upl), A_lim_upl, **hatch) if A_lim_upc: ax.fill_between(F_lim_upc, max(A_lim_upc), A_lim_upc, **hatch) if A_lim_upr: ax.fill_between(F_lim_upr, max(A_lim_upr), A_lim_upr, **hatch) # lower limits: ax.plot(F_lim_lol, A_lim_lol, F_lim_loc, A_lim_loc, F_lim_lor, A_lim_lor, **hatch_borders) if A_lim_lol: ax.fill_between(F_lim_lol, min(A_lim_lol), A_lim_lol, **hatch) if A_lim_loc: ax.fill_between(F_lim_loc, min(A_lim_loc), A_lim_loc, **hatch) if A_lim_lor: ax.fill_between(F_lim_lor, min(A_lim_lor), A_lim_lor, **hatch) if self.unitA == 'V': exp = 1. elif self.unitA == 'W': exp = 2. if self.unitA == 'dB': if fb.fil[0]['ft'] == "FIR": A_PB_max = dB(1 + self.A_PB) A_PB2_max = dB(1 + self.A_PB2) else: # IIR dB A_PB_max = A_PB2_max = 0 A_PB_min = dB(1 - self.A_PB) A_PB2_min = dB(1 - self.A_PB2) A_PB_minx = min(A_PB_min, A_PB2_min) - 5 A_PB_maxx = max(A_PB_max, A_PB2_max) + 5 A_SB = dB(self.A_SB) A_SB2 = dB(self.A_SB2) A_SB_maxx = max(A_SB, A_SB2) + 10 else: # 'V' or 'W' if fb.fil[0]['ft'] == "FIR": A_PB_max = (1 + self.A_PB)**exp A_PB2_max = (1 + self.A_PB2)**exp else: # IIR lin A_PB_max = A_PB2_max = 1 A_PB_min = (1 - self.A_PB)**exp A_PB2_min = (1 - self.A_PB2)**exp A_PB_minx = min(A_PB_min, A_PB2_min) / 1.05 A_PB_maxx = max(A_PB_max, A_PB2_max) * 1.05 A_SB = self.A_SB ** exp A_SB2 = self.A_SB2 ** exp A_SB_maxx = A_PB_min / 10. F_max = self.f_max/2 F_PB = self.F_PB F_SB = fb.fil[0]['F_SB'] * self.f_max F_SB2 = fb.fil[0]['F_SB2'] * self.f_max F_PB2 = fb.fil[0]['F_PB2'] * self.f_max F_lim_upl = F_lim_lol = [] # left side limits, lower and upper A_lim_upl = A_lim_lol = [] F_lim_upc = F_lim_loc = [] # center limits, lower and upper A_lim_upc = A_lim_loc = [] F_lim_upr = F_lim_lor = [] # right side limits, lower and upper A_lim_upr = A_lim_lor = [] if fb.fil[0]['rt'] == 'LP': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB, F_SB, F_max] A_lim_upr = [A_SB_maxx, A_SB, A_SB] if fb.fil[0]['rt'] == 'HP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB, F_PB, F_max] A_lim_upr = [A_PB_maxx, A_PB_max, A_PB_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB_min, A_PB_min] if fb.fil[0]['rt'] == 'BS': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upc = [F_SB, F_SB, F_SB2, F_SB2] A_lim_upc = [A_SB_maxx, A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB2, F_PB2, F_max] A_lim_upr = [A_PB_maxx, A_PB2_max, A_PB2_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB2_min, A_PB2_min] if fb.fil[0]['rt'] == 'BP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB2, F_SB2, F_max] A_lim_upr = [A_SB_maxx, A_SB2, A_SB2] if fb.fil[0]['rt'] == 'HIL': F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = np.array(F_lim_upr) F_lim_lor = np.array(F_lim_lor) F_lim_upl = np.array(F_lim_upl) F_lim_lol = np.array(F_lim_lol) F_lim_upc = np.array(F_lim_upc) F_lim_loc = np.array(F_lim_loc) _plot_specs() # plot specs in the range 0 ... f_S/2 if fb.fil[0]['freqSpecsRangeType'] != 'half': # add plot limits for other half of the spectrum if fb.fil[0]['freqSpecsRangeType'] == 'sym': # frequency axis +/- f_S/2 F_lim_upl = -F_lim_upl F_lim_lol = -F_lim_lol F_lim_upc = -F_lim_upc F_lim_loc = -F_lim_loc F_lim_upr = -F_lim_upr F_lim_lor = -F_lim_lor else: # -> 'whole' F_lim_upl = self.f_max - F_lim_upl F_lim_lol = self.f_max - F_lim_lol F_lim_upc = self.f_max - F_lim_upc F_lim_loc = self.f_max - F_lim_loc F_lim_upr = self.f_max - F_lim_upr F_lim_lor = self.f_max - F_lim_lor _plot_specs() #------------------------------------------------------------------------------ def draw_inset(self): """ Construct / destruct second axes for an inset second plot """ # TODO: try ax1 = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6 # TODO: choose size & position of inset, maybe dependent on filter type # or specs (i.e. where is passband etc.) # DEBUG # print(self.cmbInset.currentIndex(), self.mplwidget.fig.axes) # list of axes in Figure # for ax in self.mplwidget.fig.axes: # print(ax) # print("cmbInset, inset_idx:",self.cmbInset.currentIndex(), self.inset_idx) if self.cmbInset.currentIndex() > 0: if self.inset_idx == 0: # Inset was turned off before, create a new one # Add an axes at position rect [left, bottom, width, height]: self.ax_i = self.mplwidget.fig.add_axes([0.65, 0.61, .3, .3]) self.ax_i.clear() # clear old plot and specs # draw an opaque background with the extent of the inset plot: # self.ax_i.patch.set_facecolor('green') # without label area # self.mplwidget.fig.patch.set_facecolor('green') # whole figure extent = self.mplwidget.get_full_extent(self.ax_i, pad=0.0) # Transform this back to figure coordinates - otherwise, it # won't behave correctly when the size of the plot is changed: extent = extent.transformed(self.mplwidget.fig.transFigure.inverted()) rect = Rectangle((extent.xmin, extent.ymin), extent.width, extent.height, facecolor=rcParams['figure.facecolor'], edgecolor='none', transform=self.mplwidget.fig.transFigure, zorder=-1) self.ax_i.patches.append(rect) self.ax_i.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_i.plot(self.F, self.H_plt) if self.cmbInset.currentIndex() == 1: # edit / navigate inset self.ax_i.set_navigate(True) self.ax.set_navigate(False) if self.but_specs.isChecked(): self.plot_spec_limits(self.ax_i) else: # edit / navigate main plot self.ax_i.set_navigate(False) self.ax.set_navigate(True) else: # inset has been turned off, delete it self.ax.set_navigate(True) try: #remove ax_i from the figure self.mplwidget.fig.delaxes(self.ax_i) except AttributeError: pass self.inset_idx = self.cmbInset.currentIndex() # update index self.draw() # ------------------------------------------------------------------------------ def draw_phase(self, ax): """ Draw phase on second y-axis in the axes system passed as the argument """ if hasattr(self, 'ax_p'): self.mplwidget.fig.delaxes(self.ax_p) del self.ax_p # try: # self.mplwidget.fig.delaxes(self.ax_p) # except (KeyError, AttributeError): # pass if self.but_phase.isChecked(): self.ax_p = ax.twinx() # second axes system with same x-axis for phase self.ax_p.is_twin = True # mark this as 'twin' to suppress second grid in mpl_widget # phi_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$' if fb.fil[0]['plt_phiUnit'] == 'rad': phi_str += ' in rad ' + r'$\rightarrow $' scale = 1. elif fb.fil[0]['plt_phiUnit'] == 'rad/pi': phi_str += ' in rad' + r'$ / \pi \;\rightarrow $' scale = 1./ np.pi else: phi_str += ' in deg ' + r'$\rightarrow $' scale = 180./np.pi # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans phi = np.angle(np.nan_to_num(self.H_c)) # ----------------------------------------------------------- self.ax_p.plot(self.F, np.unwrap(phi)*scale, 'g-.', label="Phase") # ----------------------------------------------------------- self.ax_p.set_ylabel(phi_str) #------------------------------------------------------------------------------ def calc_hf(self): """ (Re-)Calculate the complex frequency response H_cmplx(W) (complex) for W = 0 ... 2 pi: """ self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], True) #------------------------------------------------------------------------------ def draw(self): """ Re-calculate \|H(f)\| and draw the figure """ self.but_align.setVisible(self.but_phase.isChecked()) self.calc_hf() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') # Get corners for spec display from the parameters of the target specs subwidget try: param_list = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']]\ [fb.fil[0]['fc']][fb.fil[0]['fo']]['tspecs'][1]['amp'] except KeyError: param_list = [] SB = [l for l in param_list if 'A_SB' in l] PB = [l for l in param_list if 'A_PB' in l] if SB: A_min = min([fb.fil[0][l] for l in SB]) else: A_min = 5e-4 if PB: A_max = max([fb.fil[0][l] for l in PB]) else: A_max = 1 if np.all(self.W) is None: # H(f) has not been calculated yet self.calc_hf() if self.cmb_units_a.currentText() == 'Auto': self.unitA = fb.fil[0]['amp_specs_unit'] else: self.unitA = self.cmb_units_a.currentText() # only display log bottom widget for unit dB self.lbl_log_bottom.setVisible(self.unitA == 'dB') self.led_log_bottom.setVisible(self.unitA == 'dB') self.lbl_log_unit.setVisible(self.unitA == 'dB') # Linphase settings only makes sense for amplitude plot and # for plottin real/imag. part of H, not its magnitude self.but_zerophase.setCheckable(self.unitA == 'V') self.but_zerophase.setEnabled(self.unitA == 'V') self.specs = self.but_specs.isChecked() self.f_max = fb.fil[0]['f_max'] self.F_PB = fb.fil[0]['F_PB'] * self.f_max self.f_maxB = fb.fil[0]['F_SB'] * self.f_max self.A_PB = fb.fil[0]['A_PB'] self.A_PB2 = fb.fil[0]['A_PB2'] self.A_SB = fb.fil[0]['A_SB'] self.A_SB2 = fb.fil[0]['A_SB2'] f_lim = fb.fil[0]['freqSpecsRange'] # ========= select frequency range to be displayed ===================== # === shift, scale and select: W -> F, H_cplx -> H_c self.F = self.W / (2 * np.pi) * self.f_max if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 self.H_c = np.fft.fftshift(self.H_cmplx) self.F -= self.f_max/2. elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F self.H_c = self.H_cmplx[0:params['N_FFT']//2] self.F = self.F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated self.H_c = self.H_cmplx # now calculate mag / real / imaginary part of H_c: if self.but_zerophase.isChecked(): # remove the linear phase self.H_c = self.H_c * np.exp(1j * self.W[0:len(self.F)] * fb.fil[0]["N"]/2.) if self.cmbShowH.currentIndex() == 0: # show magnitude of H H = abs(self.H_c) H_str = r'$|H(\mathrm{e}^{\mathrm{j} \Omega})|$' elif self.cmbShowH.currentIndex() == 1: # show real part of H H = self.H_c.real H_str = r'$\Re \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' else: # show imag. part of H H = self.H_c.imag H_str = r'$\Im \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' # ================ Main Plotting Routine ========================= # === clear the axes and (re)draw the plot (if selectable) if self.ax.get_navigate(): if self.unitA == 'dB': self.log_bottom = safe_eval( self.led_log_bottom.text(), self.log_bottom, return_type='float', sign='neg') self.led_log_bottom.setText(str(self.log_bottom)) self.H_plt = np.maximum(20*np.log10(abs(H)), self.log_bottom) A_lim = [self.log_bottom, 2] H_str += ' in dB ' + r'$\rightarrow$' elif self.unitA == 'V': # 'lin' self.H_plt = H if self.cmbShowH.currentIndex() != 0: # H can be less than zero A_min = max(self.lin_neg_bottom, np.nanmin(self.H_plt[np.isfinite(self.H_plt)])) else: A_min = 0 A_lim = [A_min, (1.05 + A_max)] H_str +=' in V ' + r'$\rightarrow $' self.ax.axhline(linewidth=1, color='k') # horizontal line at 0 else: # unit is W A_lim = [0, (1.03 + A_max)**2.] self.H_plt = H * H.conj() H_str += ' in W ' + r'$\rightarrow $' #logger.debug("lim: {0}, min: {1}, max: {2} - {3}".format(A_lim, A_min, A_max, self.H_plt[0])) #----------------------------------------------------------- self.ax.clear() self.ax.plot(self.F, self.H_plt, label = 'H(f)') # TODO: self.draw_inset() # this gives an infinite recursion self.draw_phase(self.ax) #----------------------------------------------------------- #============= Set Limits and draw specs ========================= if self.but_specs.isChecked(): self.plot_spec_limits(self.ax) # self.ax_bounds = [self.ax.get_ybound()[0], self.ax.get_ybound()[1]]#, self.ax.get] self.ax.set_xlim(f_lim) self.ax.set_ylim(A_lim) # logger.warning("set limits") self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(H_str) if self.but_phase.isChecked(): self.ax.set_title(r'Magnitude and Phase Frequency Response') else: self.ax.set_title(r'Magnitude Frequency Response') self.ax.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax.yaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks np.seterr(**old_settings_seterr) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ if hasattr(self, 'ax_p') and self.but_align.isChecked(): # Align gridlines between H(f) and phi nicely self.align_y_axes(self.ax, self.ax_p) self.mplwidget.redraw()
class Plot_FFT_win(QDialog): """ Create a pop-up widget for displaying time and frequency view of an FFT window. Data is passed via the dictionary `win_dict` that is passed during construction. """ # incoming sig_rx = pyqtSignal(object) # outgoing sig_tx = pyqtSignal(object) def __init__(self, parent, win_dict=fb.fil[0]['win_fft'], sym=True, title='pyFDA Window Viewer'): super(Plot_FFT_win, self).__init__(parent) self.needs_calc = True self.needs_draw = True self.needs_redraw = True self.bottom_f = -80 # min. value for dB display self.bottom_t = -60 self.N = 32 # initial number of data points self.N_auto = win_dict['win_len'] self.pad = 16 # amount of zero padding self.win_dict = win_dict self.sym = sym self.tbl_rows = 2 self.tbl_cols = 6 # initial settings for checkboxes self.tbl_sel = [True, True, False, False] self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(title) self._construct_UI() qwindow_stay_on_top(self, True) #------------------------------------------------------------------------------ def closeEvent(self, event): """ Catch closeEvent (user has tried to close the window) and send a signal to parent where window closing is registered before actually closing the window. """ self.sig_tx.emit({'sender': __name__, 'closeEvent': ''}) event.accept() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("PROCESS_SIG_RX - vis: {0}\n{1}"\ .format(self.isVisible(), pprint_log(dict_sig))) if ('view_changed' in dict_sig and dict_sig['view_changed'] == 'win')\ or ('filt_changed' in dict_sig and dict_sig['filt_changed'] == 'firwin')\ or self.needs_calc: # logger.warning("Auto: {0} - WinLen: {1}".format(self.N_auto, self.win_dict['win_len'])) self.N_auto = self.win_dict['win_len'] self.calc_N() if self.isVisible(): self.draw() self.needs_calc = False else: self.needs_calc = True elif 'home' in dict_sig: self.update_view() else: logger.error("Unknown content of dict_sig: {0}".format(dict_sig)) #------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.chk_auto_N = QCheckBox(self) self.chk_auto_N.setChecked(False) self.chk_auto_N.setToolTip( "Use number of points from calling routine.") self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N)) self.led_N.setMaximumWidth(70) self.led_N.setToolTip("<span>Number of window data points.</span>") self.chk_log_t = QCheckBox("Log", self) self.chk_log_t.setChecked(False) self.chk_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(50) self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel("dB", self) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.chk_norm_f = QCheckBox("Norm", self) self.chk_norm_f.setChecked(True) self.chk_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.chk_half_f = QCheckBox("Half", self) self.chk_half_f.setChecked(True) self.chk_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") self.chk_log_f = QCheckBox("Log", self) self.chk_log_f.setChecked(True) self.chk_log_f.setToolTip("Display in dB") self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(50) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_f = QLabel("dB", self) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) layHControls = QHBoxLayout() layHControls.addWidget(self.chk_auto_N) layHControls.addWidget(self.lbl_auto_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addStretch(10) layHControls.addWidget(self.chk_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_half_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.lbl_log_bottom_f) self.tblWinProperties = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tblWinProperties.setAlternatingRowColors(True) self.tblWinProperties.verticalHeader().setVisible(False) self.tblWinProperties.horizontalHeader().setVisible(False) self._init_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget: Layout layVMainMpl (VBox) is defined with MplWidget, # additional widgets can be added (like self.frmControls) # The widget encompasses all other widgets. #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. #---------------------------------------------------------------------- layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tblWinProperties) layVInfo.addWidget(self.txtInfoBox) self.frmInfo = QFrame(self) self.frmInfo.setObjectName("frmInfo") self.frmInfo.setLayout(layVInfo) #---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(self.frmInfo) # setSizes uses absolute pixel values, but can be "misused" by specifying values # that are way too large: in this case, the space is distributed according # to the _ratio_ of the values: splitter.setSizes([3000, 1000]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) #---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chk_log_f.clicked.connect(self.update_view) self.chk_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.chk_auto_N.clicked.connect(self.calc_N) self.led_N.editingFinished.connect(self.draw) self.chk_norm_f.clicked.connect(self.draw) self.chk_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tblWinProperties.itemClicked.connect(self._handle_item_clicked) #------------------------------------------------------------------------------ def _init_table(self, rows, cols, val): for r in range(rows): for c in range(cols): item = QTableWidgetItem(val) if c % 3 == 0: item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if self.tbl_sel[r * 2 + c % 3]: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self.tblWinProperties.setItem(r, c, item) # https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget #------------------------------------------------------------------------------ def _set_table_item(self, row, col, val, font=None, sel=None): """ Set the table item with the index `row, col` and the value val """ item = self.tblWinProperties.item(row, col) item.setText(str(val)) if font: self.tblWinProperties.item(row, col).setFont(font) if sel == True: item.setCheckState(Qt.Checked) if sel == False: item.setCheckState(Qt.Unchecked) # when sel is not specified, don't change anything #------------------------------------------------------------------------------ def _handle_item_clicked(self, item): if item.column() % 3 == 0: # clicked on checkbox num = item.row() * 2 + item.column() // 3 if item.checkState() == Qt.Checked: self.tbl_sel[num] = True logger.debug('"{0}:{1}" Checked'.format(item.text(), num)) else: self.tbl_sel[num] = False logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num)) elif item.column() % 3 == 1: # clicked on value field logger.info("{0:s} copied to clipboard.".format(item.text())) fb.clipboard.setText(item.text()) self.update_view() #------------------------------------------------------------------------------ def update_bottom(self): """ Update log bottom settings """ self.bottom_t = safe_eval(self.led_log_bottom_t.text(), self.bottom_t, sign='neg', return_type='float') self.led_log_bottom_t.setText(str(self.bottom_t)) self.bottom_f = safe_eval(self.led_log_bottom_f.text(), self.bottom_f, sign='neg', return_type='float') self.led_log_bottom_f.setText(str(self.bottom_f)) self.update_view() #------------------------------------------------------------------------------ def calc_N(self): """ (Re-)Calculate the number of data points when Auto N chkbox has been clicked or when the number of data points has been updated outside this class """ if self.chk_auto_N.isChecked(): self.N = self.N_auto self.draw() #------------------------------------------------------------------------------ def calc_win(self): """ (Re-)Calculate the window and its FFT """ self.led_N.setEnabled(not self.chk_auto_N.isChecked()) if not self.chk_auto_N.isChecked(): self.N = safe_eval(self.led_N.text(), self.N, sign='pos', return_type='int') # else: #self.N = self.win_dict['win_len'] self.led_N.setText(str(self.N)) self.n = np.arange(self.N) self.win = calc_window_function(self.win_dict, self.win_dict['name'], self.N, sym=self.sym) self.nenbw = self.N * np.sum(np.square(self.win)) / (np.square( np.sum(self.win))) self.cgain = np.sum(self.win) / self.N self.F = fftfreq(self.N * self.pad, d=1. / fb.fil[0]['f_S']) # use zero padding self.Win = np.abs(fft(self.win, self.N * self.pad)) if self.chk_norm_f.isChecked(): self.Win /= (self.N * self.cgain ) # correct gain for periodic signals (coherent gain) first_zero = argrelextrema(self.Win[:(self.N * self.pad) // 2], np.less) if np.shape(first_zero)[1] > 0: first_zero = first_zero[0][0] self.first_zero_f = self.F[first_zero] self.sidelobe_level = np.max( self.Win[first_zero:(self.N * self.pad) // 2]) else: self.first_zero_f = np.nan self.sidelobe_level = 0 #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate \|H(f)\| and draw the figure """ self.calc_win() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating the window. """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') self.ax_t.cla() self.ax_f.cla() self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$') self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$') if self.chk_log_t.isChecked(): self.ax_t.plot( self.n, np.maximum(20 * np.log10(np.abs(self.win)), self.bottom_t)) else: self.ax_t.plot(self.n, self.win) if self.chk_half_f.isChecked(): F = self.F[:len(self.F * self.pad) // 2] Win = self.Win[:len(self.F * self.pad) // 2] else: F = fftshift(self.F) Win = fftshift(self.Win) if self.chk_log_f.isChecked(): self.ax_f.plot( F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f)) self.nenbw_disp = 10 * np.log10(self.nenbw) self.cgain_disp = 20 * np.log10(self.cgain) self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level) self.unit_nenbw = "dB" self.unit_scale = "dB" else: self.ax_f.plot(F, Win) self.nenbw_disp = self.nenbw self.cgain_disp = self.cgain self.sidelobe_level_disp = self.sidelobe_level self.unit_nenbw = "bins" self.unit_scale = "" self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) window_name = self.win_dict['name'] param_txt = "" if self.win_dict['n_par'] > 0: param_txt = " (" + self.win_dict['par'][0][ 'name_tex'] + " = {0:.3g})".format( self.win_dict['par'][0]['val']) if self.win_dict['n_par'] > 1: param_txt = param_txt[:-1]\ + ", {0:s} = {1:.3g})".format(self.win_dict['par'][1]['name_tex'], self.win_dict['par'][1]['val']) self.mplwidget.fig.suptitle(r'{0} Window'.format(window_name) + param_txt) # plot a line at the max. sidelobe level if self.tbl_sel[3]: self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b') patch = mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0) # Info legend for time domain window labels_t = [] labels_t.append("$N$ = {0:d}".format(self.N)) self.ax_t.legend([patch], labels_t, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) # Info legend for frequency domain window labels_f = [] N_patches = 0 if self.tbl_sel[0]: labels_f.append("$NENBW$ = {0:.4g} {1}".format( self.nenbw_disp, self.unit_nenbw)) N_patches += 1 if self.tbl_sel[1]: labels_f.append("$CGAIN$ = {0:.4g} {1}".format( self.cgain_disp, self.unit_scale)) N_patches += 1 if self.tbl_sel[2]: labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f)) N_patches += 1 if N_patches > 0: self.ax_f.legend([patch] * N_patches, labels_f, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) np.seterr(**old_settings_seterr) self.update_info() self.redraw() #------------------------------------------------------------------------------ def update_info(self): """ Update the text info box for the window """ if 'info' in self.win_dict: self.txtInfoBox.setText(self.win_dict['info']) self._set_table_item(0, 0, "ENBW", font=self.bfont) #, sel=True) self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp)) self._set_table_item(0, 2, self.unit_nenbw) self._set_table_item(0, 3, "Scale", font=self.bfont) #, sel=True) self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp)) self._set_table_item(0, 5, self.unit_scale) self._set_table_item(1, 0, "1st Zero", font=self.bfont) #, sel=True) self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f)) self._set_table_item(1, 2, "f_S") self._set_table_item(1, 3, "Sidelobes", font=self.bfont) #, sel=True) self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp)) self._set_table_item(1, 5, self.unit_scale) self.tblWinProperties.resizeColumnsToContents() self.tblWinProperties.resizeRowsToContents() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw() self.needs_redraw = False
class Plot_FFT_win(QDialog): """ Create a pop-up widget for displaying time and frequency view of an FFT window. Data is passed via the dictionary `win_dict` that is specified during construction. Available windows, parameters, tooltipps etc are provided by the widget `pyfda_fft_windows_lib.QFFTWinSelection` Parameters ---------- parent : class instance reference to parent win_dict : dict dictionary derived from `pyfda_fft_windows_lib.all_windows_dict` with valid and available windows and their current settings (if applicable) sym : bool Passed to `get_window()`: When True, generate a symmetric window for use in filter design. When False (default), generate a periodic window for use in spectral analysis. title : str Title text for Qt Window ignore_close_event : bool Disable close event when True (Default) Methods ------- - `self.calc_N()` - `self.update_view()`: - `self.draw()`: calculate window and FFT and draw both - `get_win(N)` : Get the window array """ sig_rx = pyqtSignal(object) # incoming sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent, win_dict, sym=False, title='pyFDA Window Viewer', ignore_close_event=True): super(Plot_FFT_win, self).__init__(parent) # make window stay on top qwindow_stay_on_top(self, True) self.win_dict = win_dict self.sym = sym self.ignore_close_event = ignore_close_event self.setWindowTitle(title) self.needs_calc = True self.bottom_f = -80 # min. value for dB display self.bottom_t = -60 # initial number of data points for visualization self.N_view = 32 self.pad = 16 # zero padding factor for smooth FFT plot # initial settings for checkboxes self.tbl_sel = [True, True, False, False] # False, False, False, False] self.tbl_cols = 6 self.tbl_rows = len(self.tbl_sel) // (self.tbl_cols // 3) self._construct_UI() self.calc_win_draw() # ------------------------------------------------------------------------------ def closeEvent(self, event): """ Catch `closeEvent` (user has tried to close the FFT window) and send a signal to parent to decide how to proceed. This can be disabled by setting `self.ignore_close_event = False` e.g. for instantiating the widget as a standalone window. """ if self.ignore_close_event: event.ignore() self.emit({'closeEvent': ''}) # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx: - `self.calc_N` - `self.update_view`: - `self.draw`: calculate window and FFT and draw both """ # logger.debug("PROCESS_SIG_RX - vis={0}, needs_calc={1}\n{2}" # .format(self.isVisible(), self.needs_calc, pprint_log(dict_sig))) if dict_sig['id'] == id(self): logger.warning("Stopped infinite loop:\n{0}".format( pprint_log(dict_sig))) return elif not self.isVisible(): self.needs_calc = True elif 'view_changed' in dict_sig and 'fft_win' in dict_sig['view_changed']\ or self.needs_calc: self.calc_win_draw() self.needs_calc = False elif 'home' in dict_sig: self.update_view() else: logger.error("Unknown content of dict_sig: {0}".format(dict_sig)) # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.bfont = QFont() self.bfont.setBold(True) self.qfft_win_select = QFFTWinSelector(self, self.win_dict) self.lbl_N = QLabel(to_html("N =", frmt='bi')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N_view)) self.led_N.setMaximumWidth(qtext_width(N_x=8)) self.led_N.setToolTip( "<span>Number of window data points to display.</span>") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_t = QPushButton("dB", default=False, autoDefault=False) self.but_log_t.setMaximumWidth(qtext_width(" dB ")) self.but_log_t.setObjectName("chk_log_time") self.but_log_t.setCheckable(True) self.but_log_t.setChecked(False) self.but_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.but_norm_f = QPushButton("Max=1", default=False, autoDefault=False) self.but_norm_f.setCheckable(True) self.but_norm_f.setChecked(True) self.but_norm_f.setMaximumWidth(qtext_width(text=" Max=1 ")) self.but_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.but_half_f = QPushButton("0...½", default=False, autoDefault=False) self.but_half_f.setCheckable(True) self.but_half_f.setChecked(True) self.but_half_f.setMaximumWidth(qtext_width(text=" 0...½ ")) self.but_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") # By default, the enter key triggers the default 'dialog action' in QDialog # widgets. This activates one of the pushbuttons. self.but_log_f = QPushButton("dB", default=False, autoDefault=False) self.but_log_f.setMaximumWidth(qtext_width(" dB ")) self.but_log_f.setObjectName("chk_log_freq") self.but_log_f.setToolTip("<span>Display in dB.</span>") self.but_log_f.setCheckable(True) self.but_log_f.setChecked(True) self.lbl_log_bottom_f = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked()) self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(qtext_width(N_x=6)) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") # ---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets # ---------------------------------------------------------------------- layH_win_select = QHBoxLayout() layH_win_select.addWidget(self.qfft_win_select) layH_win_select.setContentsMargins(0, 0, 0, 0) layH_win_select.addStretch(1) frmQFFT = QFrame(self) frmQFFT.setObjectName("frmQFFT") frmQFFT.setLayout(layH_win_select) hline = QHLine() layHControls = QHBoxLayout() layHControls.addWidget(self.lbl_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.but_log_t) layHControls.addStretch(5) layHControls.addWidget(QVLine(width=2)) layHControls.addStretch(5) layHControls.addWidget(self.but_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.but_half_f) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.but_log_f) layVControls = QVBoxLayout() layVControls.addWidget(frmQFFT) layVControls.addWidget(hline) layVControls.addLayout(layHControls) frmControls = QFrame(self) frmControls.setObjectName("frmControls") frmControls.setLayout(layVControls) # ---------------------------------------------------------------------- # ### mplwidget ### # # Layout layVMainMpl (VBox) is defined within MplWidget, additional # widgets can be added below the matplotlib widget (here: frmControls) # # ---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(frmControls) self.mplwidget.layVMainMpl.setContentsMargins(0, 0, 0, 0) # ---------------------------------------------------------------------- # ### frmInfo ### # # This widget encompasses the text info box and the table with window # parameters. # ---------------------------------------------------------------------- self.tbl_win_props = QTableWidget(self.tbl_rows, self.tbl_cols, self) self.tbl_win_props.setAlternatingRowColors(True) # Auto-resize of table can be set using the header (although it is invisible) self.tbl_win_props.verticalHeader().setSectionResizeMode( QHeaderView.Stretch) # Only the columns with data are stretched, the others are minimum size self.tbl_win_props.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) self.tbl_win_props.horizontalHeader().setSectionResizeMode( 4, QHeaderView.Stretch) self.tbl_win_props.verticalHeader().setVisible(False) self.tbl_win_props.horizontalHeader().setVisible(False) self.tbl_win_props.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.tbl_win_props.setFixedHeight( self.tbl_win_props.rowHeight(0) * self.tbl_rows + self.tbl_win_props.frameWidth() * 2) # self.tbl_win_props.setVerticalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) # self.tbl_win_props.setHorizontalScrollBarPolicy( # Qt.ScrollBarAlwaysOff) self._construct_table(self.tbl_rows, self.tbl_cols, " ") self.txtInfoBox = QTextBrowser(self) layVInfo = QVBoxLayout(self) layVInfo.addWidget(self.tbl_win_props) layVInfo.addWidget(self.txtInfoBox) frmInfo = QFrame(self) frmInfo.setObjectName("frmInfo") frmInfo.setLayout(layVInfo) # ---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all subwidgets # ---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(frmInfo) # setSizes uses absolute pixel values, but can be "misused" by # specifying values that are way too large: in this case, the space # is distributed according to the _ratio_ of the values: splitter.setSizes([3000, 800]) layVMain = QVBoxLayout() layVMain.addWidget(splitter) self.setLayout(layVMain) # ---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.calc_win_draw() # initial calculation and drawing # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) self.sig_rx.connect(self.qfft_win_select.sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_log_f.clicked.connect(self.update_view) self.but_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.led_N.editingFinished.connect(self.calc_win_draw) self.but_norm_f.clicked.connect(self.calc_win_draw) self.but_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.tbl_win_props.itemClicked.connect(self._handle_item_clicked) self.qfft_win_select.sig_tx.connect(self.update_fft_win) # ------------------------------------------------------------------------------ def _construct_table(self, rows, cols, val): """ Create a table with `rows` and `cols`, organized in sets of 3: Name (with a checkbox) - value - unit each item. Parameters ---------- rows : int number of rows cols : int number of columns (must be multiple of 3) val : str initialization value for the table Returns ------- None """ for r in range(rows): for c in range(cols): item = QTableWidgetItem(val) if c % 3 == 0: item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if self.tbl_sel[r * 2 + c % 3]: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self.tbl_win_props.setItem(r, c, item) # https://stackoverflow.com/questions/12366521/pyqt-checkbox-in-qtablewidget # ------------------------------------------------------------------------------ def update_fft_win(self, dict_sig=None): """ Update FFT window when window or parameters have changed and pass thru 'view_changed':'fft_win_type' or 'fft_win_par' """ self.calc_win_draw() self.emit(dict_sig) # ------------------------------------------------------------------------------ def calc_win_draw(self): """ (Re-)Calculate the window, its FFT and some characteristic values and update the plot of the window and its FFT. This should be triggered when the window type or length or a parameters has been changed. Returns ------- None Attributes ---------- """ self.N_view = safe_eval(self.led_N.text(), self.N_view, sign='pos', return_type='int') self.led_N.setText(str(self.N_view)) self.n = np.arange(self.N_view) self.win_view = self.qfft_win_select.get_window(self.N_view, sym=self.sym) if self.qfft_win_select.err: self.qfft_win_select.dict2ui() self.nenbw = self.N_view * np.sum(np.square(self.win_view))\ / np.square(np.sum(self.win_view)) self.cgain = np.sum(self.win_view) / self.N_view # coherent gain # calculate the FFT of the window with a zero padding factor # of `self.pad` self.F = fftfreq(self.N_view * self.pad, d=1. / fb.fil[0]['f_S']) self.Win = np.abs(fft(self.win_view, self.N_view * self.pad)) # Correct gain for periodic signals (coherent gain) if self.but_norm_f.isChecked(): self.Win /= (self.N_view * self.cgain) # calculate frequency of first zero and maximum sidelobe level first_zero = argrelextrema(self.Win[:(self.N_view * self.pad) // 2], np.less) if np.shape(first_zero)[1] > 0: first_zero = first_zero[0][0] self.first_zero_f = self.F[first_zero] self.sidelobe_level = np.max( self.Win[first_zero:(self.N_view * self.pad) // 2]) else: self.first_zero_f = np.nan self.sidelobe_level = 0 self.update_view() # ------------------------------------------------------------------------------ def _set_table_item(self, row, col, val, font=None, sel=None): """ Set the table item with the index `row, col` and the value val """ item = self.tbl_win_props.item(row, col) item.setText(str(val)) if font: self.tbl_win_props.item(row, col).setFont(font) if sel is True: item.setCheckState(Qt.Checked) if sel is False: item.setCheckState(Qt.Unchecked) # when sel is not specified, don't change anything # ------------------------------------------------------------------------------ def _handle_item_clicked(self, item): if item.column() % 3 == 0: # clicked on checkbox num = item.row() * 2 + item.column() // 3 if item.checkState() == Qt.Checked: self.tbl_sel[num] = True logger.debug('"{0}:{1}" Checked'.format(item.text(), num)) else: self.tbl_sel[num] = False logger.debug('"{0}:{1}" Unchecked'.format(item.text(), num)) elif item.column() % 3 == 1: # clicked on value field logger.info("{0:s} copied to clipboard.".format(item.text())) fb.clipboard.setText(item.text()) self.update_view() # ------------------------------------------------------------------------------ def update_bottom(self): """ Update log bottom settings """ self.bottom_t = safe_eval(self.led_log_bottom_t.text(), self.bottom_t, sign='neg', return_type='float') self.led_log_bottom_t.setText(str(self.bottom_t)) self.bottom_f = safe_eval(self.led_log_bottom_f.text(), self.bottom_f, sign='neg', return_type='float') self.led_log_bottom_f.setText(str(self.bottom_f)) self.update_view() # ------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale, lin/log etc without recalculating the window or its FFT. """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') self.ax_t.cla() self.ax_f.cla() self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$') self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$') if self.but_log_t.isChecked(): self.ax_t.plot( self.n, np.maximum(20 * np.log10(np.abs(self.win_view)), self.bottom_t)) else: self.ax_t.plot(self.n, self.win_view) if self.but_half_f.isChecked(): F = self.F[:len(self.F * self.pad) // 2] Win = self.Win[:len(self.F * self.pad) // 2] else: F = fftshift(self.F) Win = fftshift(self.Win) if self.but_log_f.isChecked(): self.ax_f.plot( F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f)) self.nenbw_disp = 10 * np.log10(self.nenbw) self.cgain_disp = 20 * np.log10(self.cgain) self.sidelobe_level_disp = 20 * np.log10(self.sidelobe_level) self.nenbw_unit = "dB" self.cgain_unit = "dB" else: self.ax_f.plot(F, Win) self.nenbw_disp = self.nenbw self.cgain_disp = self.cgain self.sidelobe_level_disp = self.sidelobe_level self.nenbw_unit = "bins" self.cgain_unit = "" self.led_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.lbl_log_bottom_t.setVisible(self.but_log_t.isChecked()) self.led_log_bottom_f.setVisible(self.but_log_f.isChecked()) self.lbl_log_bottom_f.setVisible(self.but_log_f.isChecked()) cur = self.win_dict['cur_win_name'] cur_win_d = self.win_dict[cur] param_txt = "" if cur_win_d['n_par'] > 0: if type(cur_win_d['par'][0]['val']) in {str}: p1 = cur_win_d['par'][0]['val'] else: p1 = "{0:.3g}".format(cur_win_d['par'][0]['val']) param_txt = " ({0:s} = {1:s})".format( cur_win_d['par'][0]['name_tex'], p1) if self.win_dict[cur]['n_par'] > 1: if type(cur_win_d['par'][1]['val']) in {str}: p2 = cur_win_d['par'][1]['val'] else: p2 = "{0:.3g}".format(cur_win_d['par'][1]['val']) param_txt = param_txt[:-1]\ + ", {0:s} = {1:s})".format(cur_win_d['par'][1]['name_tex'], p2) self.mplwidget.fig.suptitle(r'{0} Window'.format(cur) + param_txt) # plot a line at the max. sidelobe level if self.tbl_sel[3]: self.ax_f.axhline(self.sidelobe_level_disp, ls='dotted', c='b') patch = mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0) # Info legend for time domain window labels_t = [] labels_t.append("$N$ = {0:d}".format(self.N_view)) self.ax_t.legend([patch], labels_t, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) # Info legend for frequency domain window labels_f = [] N_patches = 0 if self.tbl_sel[0]: labels_f.append("$NENBW$ = {0:.4g} {1}".format( self.nenbw_disp, self.nenbw_unit)) N_patches += 1 if self.tbl_sel[1]: labels_f.append("$CGAIN$ = {0:.4g} {1}".format( self.cgain_disp, self.cgain_unit)) N_patches += 1 if self.tbl_sel[2]: labels_f.append("1st Zero = {0:.4g}".format(self.first_zero_f)) N_patches += 1 if N_patches > 0: self.ax_f.legend([patch] * N_patches, labels_f, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) np.seterr(**old_settings_seterr) self.update_info() self.redraw() # ------------------------------------------------------------------------------ def update_info(self): """ Update the text info box for the window """ cur = self.win_dict['cur_win_name'] if 'info' in self.win_dict[cur]: self.txtInfoBox.setText(self.win_dict[cur]['info']) else: self.txtInfoBox.clear() self._set_table_item(0, 0, "NENBW", font=self.bfont) # , sel=True) self._set_table_item(0, 1, "{0:.5g}".format(self.nenbw_disp)) self._set_table_item(0, 2, self.nenbw_unit) self._set_table_item(0, 3, "Scale", font=self.bfont) # , sel=True) self._set_table_item(0, 4, "{0:.5g}".format(self.cgain_disp)) self._set_table_item(0, 5, self.cgain_unit) self._set_table_item(1, 0, "1st Zero", font=self.bfont) # , sel=True) self._set_table_item(1, 1, "{0:.5g}".format(self.first_zero_f)) self._set_table_item(1, 2, "f_S") self._set_table_item(1, 3, "Sidelobes", font=self.bfont) # , sel=True) self._set_table_item(1, 4, "{0:.5g}".format(self.sidelobe_level_disp)) self._set_table_item(1, 5, self.cgain_unit) self.tbl_win_props.resizeColumnsToContents() self.tbl_win_props.resizeRowsToContents() # ----------------------------------------------------------------------------- def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class Plot_PZ(QWidget): # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self): super().__init__() self.needs_calc = True # flag whether filter data has been changed self.needs_draw = False # flag whether whether figure needs to be drawn # with new limits etc. (not implemented yet) self.tool_tip = "Pole / zero plan" self.tab_label = "P / Z" self.cmb_overlay_items = [ "<span>Add various overlays to P/Z diagram.</span>", ("none", "None", ""), ("h(f)", "|H(f)|", "<span>Show |H(f)| wrapped around the unit circle between 0 resp. -120 dB " "and max(H(f)).</span>"), ("contour", "Contour", "<span>Show contour lines for |H(z)|</span>"), ("contourf", "Contourf", "<span>Show filled contours for |H(z)|</span>"), ] self.cmb_overlay_default = "none" # default setting self.cmap = "viridis" # colormap self.zmin = 0 self.zmax = 2 self.zmin_dB = -80 self.zmax_dB = np.round(20 * np.log10(self.zmax), 2) self._construct_UI() # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig: dict = None) -> None: """ Process signals coming from the navigation toolbar and from sig_rx """ # logger.info("Processing {0} | needs_draw = {1}, visible = {2}"\ # .format(dict_sig, self.needs_calc, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False self.needs_draw = False elif 'view_changed' in dict_sig or self.needs_draw: self.update_view() self.needs_draw = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.draw() else: if 'data_changed' in dict_sig: self.needs_calc = True elif 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.needs_draw = True # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.lbl_overlay = QLabel(to_html("Overlay:", frmt='bi'), self) self.cmb_overlay = QComboBox(self) qcmb_box_populate( self.cmb_overlay, self.cmb_overlay_items, self.cmb_overlay_default) self.but_log = PushButton(" Log.", checked=True) self.but_log.setObjectName("but_log") self.but_log.setToolTip("<span>Log. scale for overlays.</span>") self.diaRad_Hf = QDial(self) self.diaRad_Hf.setRange(2, 10) self.diaRad_Hf.setValue(2) self.diaRad_Hf.setTracking(False) # produce less events when turning self.diaRad_Hf.setFixedHeight(30) self.diaRad_Hf.setFixedWidth(30) self.diaRad_Hf.setWrapping(False) self.diaRad_Hf.setToolTip("<span>Set max. radius for |H(f)| plot.</span>") self.lblRad_Hf = QLabel("Radius", self) self.lblBottom = QLabel(to_html("Bottom =", frmt='bi'), self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setMaximumWidth(qtext_width(N_x=8)) self.ledBottom.setToolTip("Minimum display value.") self.lblBottomdB = QLabel("dB", self) self.lblBottomdB.setVisible(self.but_log.isChecked()) self.lblTop = QLabel(to_html("Top =", frmt='bi'), self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.ledTop.setMaximumWidth(qtext_width(N_x=8)) self.lblTopdB = QLabel("dB", self) self.lblTopdB.setVisible(self.but_log.isChecked()) self.but_fir_poles = PushButton(" FIR Poles ", checked=True) self.but_fir_poles.setToolTip("<span>Show FIR poles at the origin.</span>") layHControls = QHBoxLayout() layHControls.addWidget(self.lbl_overlay) layHControls.addWidget(self.cmb_overlay) layHControls.addWidget(self.but_log) layHControls.addWidget(self.diaRad_Hf) layHControls.addWidget(self.lblRad_Hf) layHControls.addWidget(self.lblTop) layHControls.addWidget(self.ledTop) layHControls.addWidget(self.lblTopdB) layHControls.addWidget(self.lblBottom) layHControls.addWidget(self.ledBottom) layHControls.addWidget(self.lblBottomdB) layHControls.addStretch(10) layHControls.addWidget(self.but_fir_poles) # ---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets # ---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) # ---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets # ---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_pz.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self._log_clicked() # calculate and draw poles and zeros # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.cmb_overlay.currentIndexChanged.connect(self.draw) self.but_log.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.diaRad_Hf.valueChanged.connect(self.draw) self.but_fir_poles.clicked.connect(self.draw) # -------------------------------------------------------------------------- def _log_clicked(self): """ Change scale and settings to log / lin when log setting is changed Update min / max settings when lineEdits have been edited """ # clicking but_log triggered the slot or initialization if self.sender() is None or self.sender().objectName() == 'but_log': if self.but_log.isChecked(): self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = np.round(20 * np.log10(self.zmax), 2) self.ledTop.setText(str(self.zmax_dB)) else: self.ledBottom.setText(str(self.zmin)) self.zmax = np.round(10**(self.zmax_dB / 20), 2) self.ledTop.setText(str(self.zmax)) else: # finishing a lineEdit field triggered the slot if self.but_log.isChecked(): self.zmin_dB = safe_eval( self.ledBottom.text(), self.zmin_dB, return_type='float') self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = safe_eval( self.ledTop.text(), self.zmax_dB, return_type='float') self.ledTop.setText(str(self.zmax_dB)) else: self.zmin = safe_eval( self.ledBottom.text(), self.zmin, return_type='float') self.ledBottom.setText(str(self.zmin)) self.zmax = safe_eval(self.ledTop.text(), self.zmax, return_type='float') self.ledTop.setText(str(self.zmax)) self.draw() # -------------------------------------------------------------------------- def init_axes(self): """ Initialize and clear the axes (this is only run once) """ self.mplwidget.fig.clf() # needed to get rid of colorbar if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes self.ax = self.mplwidget.fig.subplots() # .add_subplot(111) self.ax.xaxis.tick_bottom() # remove axis ticks on top self.ax.yaxis.tick_left() # remove axis ticks right # -------------------------------------------------------------------------- def update_view(self): """ Draw the figure with new limits, scale etcs without recalculating H(f) -- not yet implemented, just use draw() for the moment """ self.draw() # -------------------------------------------------------------------------- def draw(self): self.but_fir_poles.setVisible(fb.fil[0]['ft'] == 'FIR') contour = qget_cmb_box(self.cmb_overlay) in {"contour", "contourf"} self.ledBottom.setVisible(contour) self.lblBottom.setVisible(contour) self.lblBottomdB.setVisible(contour and self.but_log.isChecked()) self.ledTop.setVisible(contour) self.lblTop.setVisible(contour) self.lblTopdB.setVisible(contour and self.but_log.isChecked()) if True: self.init_axes() self.draw_pz() # -------------------------------------------------------------------------- def draw_pz(self): """ (re)draw P/Z plot """ p_marker = params['P_Marker'] z_marker = params['Z_Marker'] zpk = fb.fil[0]['zpk'] # add antiCausals if they exist (must take reciprocal to plot) if 'rpk' in fb.fil[0]: zA = fb.fil[0]['zpk'][0] zA = np.conj(1./zA) pA = fb.fil[0]['zpk'][1] pA = np.conj(1./pA) zC = np.append(zpk[0], zA) pC = np.append(zpk[1], pA) zpk[0] = zC zpk[1] = pC self.ax.clear() [z, p, k] = self.zplane( z=zpk[0], p=zpk[1], k=zpk[2], plt_ax=self.ax, plt_poles=self.but_fir_poles.isChecked() or fb.fil[0]['ft'] == 'IIR', mps=p_marker[0], mpc=p_marker[1], mzs=z_marker[0], mzc=z_marker[1]) self.ax.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax.yaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax.set_title(r'Pole / Zero Plot') self.ax.set_xlabel('Real axis') self.ax.set_ylabel('Imaginary axis') overlay = qget_cmb_box(self.cmb_overlay) self.but_log.setVisible(overlay != "none") self.draw_Hf(r=self.diaRad_Hf.value(), Hf_visible=(overlay == "h(f)")) self.draw_contours(overlay) self.redraw() # -------------------------------------------------------------------------- def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw() # -------------------------------------------------------------------------- def zplane(self, b=None, a=1, z=None, p=None, k=1, pn_eps=1e-3, analog=False, plt_ax=None, plt_poles=True, style='equal', anaCircleRad=0, lw=2, mps=10, mzs=10, mpc='r', mzc='b', plabel='', zlabel=''): """ Plot the poles and zeros in the complex z-plane either from the coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False) or directly from the zeros and poles (z,p) (zpk = True). When only b is given, an FIR filter with all poles at the origin is assumed. Parameters ---------- b : array_like Numerator coefficients (transversal part of filter) When b is not None, poles and zeros are determined from the coefficients b and a a : array_like (optional, default = 1 for FIR-filter) Denominator coefficients (recursive part of filter) z : array_like, default = None Zeros When b is None, poles and zeros are taken directly from z and p p : array_like, default = None Poles analog : boolean (default: False) When True, create a P/Z plot suitable for the s-plane, i.e. suppress the unit circle (unless anaCircleRad > 0) and scale the plot for a good display of all poles and zeros. pn_eps : float (default : 1e-2) Tolerance for separating close poles or zeros plt_ax : handle to axes for plotting (default: None) When no axes is specified, the current axes is determined via plt.gca() plt_poles : Boolean (default : True) Plot poles. This can be used to suppress poles for FIR systems where all poles are at the origin. style : string (default: 'scaled') Style of the plot, for `style == 'scaled'` make scale of x- and y- axis equal, `style == 'equal'` forces x- and y-axes to be equal. This is passed as an argument to the matplotlib `ax.axis(style)` mps : integer (default: 10) Size for pole marker mzs : integer (default: 10) Size for zero marker mpc : char (default: 'r') Pole marker colour mzc : char (default: 'b') Zero marker colour lw : integer (default: 2) Linewidth for unit circle plabel, zlabel : string (default: '') This string is passed to the plot command for poles and zeros and can be displayed by legend() Returns ------- z, p, k : ndarray Notes ----- """ # TODO: # - polar option # - add keywords for color of circle -> **kwargs # - add option for multi-dimensional arrays and zpk data # make sure that all inputs are (at least 1D) arrays b = np.atleast_1d(b) a = np.atleast_1d(a) z = np.atleast_1d(z) p = np.atleast_1d(p) if b.any(): # coefficients were specified if len(b) < 2 and len(a) < 2: logger.error('No proper filter coefficients: both b and a are scalars!') return z, p, k # The coefficients are less than 1, normalize the coefficients if np.max(b) > 1: kn = np.max(b) b = b / float(kn) else: kn = 1. if np.max(a) > 1: kd = np.max(a) a = a / abs(kd) else: kd = 1. # Calculate the poles, zeros and scaling factor p = np.roots(a) z = np.roots(b) k = kn/kd elif not (len(p) or len(z)): # P/Z were specified logger.error('Either b,a or z,p must be specified!') return z, p, k # find multiple poles and zeros and their multiplicities if len(p) < 2: # single pole, [None] or [0] if not p or p == 0: # only zeros, create equal number of poles at origin p = np.array(0, ndmin=1) num_p = np.atleast_1d(len(z)) else: num_p = [1.] # single pole != 0 else: # p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg') p, num_p = unique_roots(p, tol=pn_eps, rtype='avg') # p = np.array(p); num_p = np.ones(len(p)) if len(z) > 0: z, num_z = unique_roots(z, tol=pn_eps, rtype='avg') # z = np.array(z); num_z = np.ones(len(z)) # z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg') else: num_z = [] if analog is False: # create the unit circle for the z-plane uc = patches.Circle((0, 0), radius=1, fill=False, color='grey', ls='solid', zorder=1) plt_ax.add_patch(uc) plt_ax.axis(style) # ax.spines['left'].set_position('center') # ax.spines['bottom'].set_position('center') # ax.spines['right'].set_visible(True) # ax.spines['top'].set_visible(True) else: # s-plane if anaCircleRad > 0: # plot a circle with radius = anaCircleRad uc = patches.Circle((0, 0), radius=anaCircleRad, fill=False, color='grey', ls='solid', zorder=1) plt_ax.add_patch(uc) # plot real and imaginary axis plt_ax.axhline(lw=2, color='k', zorder=1) plt_ax.axvline(lw=2, color='k', zorder=1) # Plot the zeros plt_ax.scatter(z.real, z.imag, s=mzs*mzs, zorder=2, marker='o', facecolor='none', edgecolor=mzc, lw=lw, label=zlabel) # and print their multiplicity for i in range(len(z)): logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i])) if num_z[i] > 1: plt_ax.text(np.real(z[i]), np.imag(z[i]), ' (' + str(num_z[i]) + ')', va='top', color=mzc) if plt_poles: # Plot the poles plt_ax.scatter(p.real, p.imag, s=mps*mps, zorder=2, marker='x', color=mpc, lw=lw, label=plabel) # and print their multiplicity for i in range(len(p)): logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i])) if num_p[i] > 1: plt_ax.text(np.real(p[i]), np.imag(p[i]), ' (' + str(num_p[i]) + ')', va='bottom', color=mpc) # ============================================================================= # # increase distance between ticks and labels # # to give some room for poles and zeros # for tick in ax.get_xaxis().get_major_ticks(): # tick.set_pad(12.) # tick.label1 = tick._get_text1() # for tick in ax.get_yaxis().get_major_ticks(): # tick.set_pad(12.) # tick.label1 = tick._get_text1() # # ============================================================================= xl = plt_ax.get_xlim() Dx = max(abs(xl[1]-xl[0]), 0.05) yl = plt_ax.get_ylim() Dy = max(abs(yl[1]-yl[0]), 0.05) plt_ax.set_xlim((xl[0]-Dx*0.02, max(xl[1]+Dx*0.02, 0))) plt_ax.set_ylim((yl[0]-Dy*0.02, yl[1] + Dy*0.02)) return z, p, k # -------------------------------------------------------------------------- def draw_contours(self, overlay): if overlay not in {"contour", "contourf"}: return self.ax.apply_aspect() # normally, the correct aspect is only set when plotting xl = self.ax.get_xlim() yl = self.ax.get_ylim() # logger.warning(xl) # logger.warning(yl) [x, y] = np.meshgrid( np.arange(xl[0], xl[1], 0.01), np.arange(yl[0], yl[1], 0.01)) z = x + 1j*y # create coordinate grid for complex plane if self.but_log.isChecked(): H_max = self.zmax_dB H_min = self.zmin_dB else: H_max = self.zmax H_min = self.zmin Hmag = H_mag(fb.fil[0]['ba'][0], fb.fil[0]['ba'][1], z, H_max, H_min=H_min, log=self.but_log.isChecked()) if overlay == "contour": self.ax.contour(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap) else: self.ax.contourf(x, y, Hmag, 20, alpha=0.5, cmap=self.cmap) m_cb = cm.ScalarMappable(cmap=self.cmap) # normalized proxy object that is m_cb.set_array(Hmag) # mappable for colorbar (?) self.col_bar = self.mplwidget.fig.colorbar( m_cb, ax=self.ax, shrink=1.0, aspect=40, pad=0.01, fraction=0.08) # Contour plots and color bar somehow mess up the coordinates: # restore to previous settings self.ax.set_xlim(xl) self.ax.set_xlim(yl) # -------------------------------------------------------------------------- def draw_Hf(self, r=2, Hf_visible=True): """ Draw the magnitude frequency response around the UC """ self.diaRad_Hf.setVisible(Hf_visible) self.lblRad_Hf.setVisible(Hf_visible) if not Hf_visible: return # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') ba = fb.fil[0]['ba'] w, H = sig.freqz(ba[0], ba[1], worN=params['N_FFT'], whole=True) H = np.abs(H) if self.but_log.isChecked(): H = np.clip(np.log10(H), -6, None) # clip to -120 dB H = H - np.max(H) # shift scale to H_min ... 0 H = 1 + (r-1) * (1 + H / abs(np.min(H))) # scale to 1 ... r else: H = 1 + (r-1) * H / np.max(H) # map |H(f)| to a range 1 ... r y = H * np.sin(w) x = H * np.cos(w) self.ax.plot(x, y, label="|H(f)|") uc = patches.Circle((0, 0), radius=r, fill=False, color='grey', ls='dashed', zorder=1) self.ax.add_patch(uc) xl = self.ax.get_xlim() xmax = max(abs(xl[0]), abs(xl[1]), r*1.05) yl = self.ax.get_ylim() ymax = max(abs(yl[0]), abs(yl[1]), r*1.05) self.ax.set_xlim((-xmax, xmax)) self.ax.set_ylim((-ymax, ymax)) np.seterr(**old_settings_seterr)