def __init__(self, parent): super(PlotTauG, self).__init__(parent) self.verbose = False # suppress warnings # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # # ============================================================================= self.mplwidget = MplWidget(self) # self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # mplwidget #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.ui) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # frequency widgets require special handling as they are scaled with f_s self.ui.ledFreq1.installEventFilter(self) self.ui.ledFreq2.installEventFilter(self) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) # connect to toolbar self.ui.sig_tx.connect(self.process_signals) # connect to widgets and signals upstream self.draw() # initial calculation and drawing
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.cmbUnitsPhi = QComboBox(self) units = ["rad", "rad/pi", "deg"] scales = [1., 1. / np.pi, 180. / np.pi] for unit, scale in zip(units, scales): self.cmbUnitsPhi.addItem(unit, scale) self.cmbUnitsPhi.setObjectName("cmbUnitsA") self.cmbUnitsPhi.setToolTip("Set unit for phase.") self.cmbUnitsPhi.setCurrentIndex(0) self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkWrap = QCheckBox("Wrapped Phase", self) self.chkWrap.setChecked(False) self.chkWrap.setToolTip("Plot phase wrapped to +/- pi") layHControls = QHBoxLayout() layHControls.addWidget(self.cmbUnitsPhi) layHControls.addWidget(self.chkWrap) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkWrap.clicked.connect(self.draw) self.cmbUnitsPhi.currentIndexChanged.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.cmbUnitsPhi = QComboBox(self) units = ["rad", "rad/pi", "deg"] scales = [1., 1./ np.pi, 180./np.pi] for unit, scale in zip(units, scales): self.cmbUnitsPhi.addItem(unit, scale) self.cmbUnitsPhi.setObjectName("cmbUnitsA") self.cmbUnitsPhi.setToolTip("Set unit for phase.") self.cmbUnitsPhi.setCurrentIndex(0) self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkWrap = QCheckBox("Wrapped Phase", self) self.chkWrap.setChecked(False) self.chkWrap.setToolTip("Plot phase wrapped to +/- pi") layHControls = QHBoxLayout() layHControls.addWidget(self.cmbUnitsPhi) layHControls.addWidget(self.chkWrap) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkWrap.clicked.connect(self.draw) self.cmbUnitsPhi.currentIndexChanged.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkWarnings = QCheckBox(self.tr("Verbose"), self) self.chkWarnings.setChecked(self.verbose) self.chkWarnings.setToolTip( self. tr("<span>Print messages about singular group delay and calculation times." "</span>")) self.cmbAlgorithm = QComboBox(self) qcmb_box_populate(self.cmbAlgorithm, self.cmb_algorithm_items, self.algorithm) layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) layHControls.addWidget(self.cmbAlgorithm) # This widget encompasses all control subwidgets: self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.cmbAlgorithm.currentIndexChanged.connect(self.draw)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkHf = QCheckBox("Show |H(f)|", self) self.chkHf.setToolTip("<span>Enable display of |H(f)|.</span>") self.chkHf.setEnabled(True) layHControls = QHBoxLayout() layHControls.addWidget(self.chkHf) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) # make this the central widget, taking all available space: # self.setCentralWidget(self.mplwidget) self.init_axes() self.draw() # calculate and draw poles and zeros #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) self.chkHf.clicked.connect(self.draw)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements (currently commented out) """ # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # self.chkScipy = QCheckBox("Scipy", self) # self.chkScipy.setChecked(False) # self.chkScipy.setToolTip("Use scipy group delay routine") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # ============================================================================= self.mplwidget = MplWidget(self) #self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements (currently commented out) """ # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # self.chkScipy = QCheckBox("Scipy", self) # self.chkScipy.setChecked(False) # self.chkScipy.setToolTip("Use scipy group delay routine") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # ============================================================================= self.mplwidget = MplWidget(self) #self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
def _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 _construct_UI(self): self.chkLog = QCheckBox(self) self.chkLog.setObjectName("chkLog") self.chkLog.setToolTip("<span>Logarithmic scale for y-axis.</span>") self.chkLog.setChecked(False) self.lblLog = QLabel("Log. y-axis", self) self.lblLogBottom = QLabel("Bottom = ", self) self.ledLogBottom = QLineEdit(self) self.ledLogBottom.setText(str(self.bottom)) self.ledLogBottom.setToolTip("<span>Minimum display value for log. scale.</span>") self.lbldB = QLabel("dB") self.lblPltStim = QLabel(self) self.lblPltStim.setText("Stimulus:") self.chkPltStim = QCheckBox("Show", self) self.chkPltStim.setChecked(False) self.chkPltStim.setToolTip("Show stimulus signal.") self.lblStimulus = QLabel("Type = ", self) self.cmbStimulus = QComboBox(self) self.cmbStimulus.addItems(["Pulse","Step","StepErr", "Cos", "Sine", "Rect", "Saw", "RandN", "RandU"]) self.cmbStimulus.setToolTip("Select stimulus type.") self.lblAmp = QLabel("<i>A</i> =", self) self.ledAmp = QLineEdit(self) self.ledAmp.setText(str(self.A)) self.ledAmp.setToolTip("Stimulus amplitude.") self.ledAmp.setObjectName("stimAmp") self.lblFreq = QLabel("<i>f</i> =", self) self.ledFreq = QLineEdit(self) self.ledFreq.setText(str(self.stim_freq)) self.ledFreq.setToolTip("Stimulus frequency.") self.ledFreq.setObjectName("stimFreq") self.lblFreqUnit = QLabel("f_S", self) self.lblNPoints = QLabel("<i>N</i> =", self) self.ledNPoints = QLineEdit(self) self.ledNPoints.setText("0") self.ledNPoints.setToolTip("<span>Number of points to calculate and display. " "N = 0 selects automatically.</span>") layHControls = QHBoxLayout() layHControls.addWidget(self.lblNPoints) layHControls.addWidget(self.ledNPoints) layHControls.addStretch(2) layHControls.addWidget(self.chkLog) layHControls.addWidget(self.lblLog) layHControls.addStretch(1) layHControls.addWidget(self.lblLogBottom) layHControls.addWidget(self.ledLogBottom) layHControls.addWidget(self.lbldB) layHControls.addStretch(2) layHControls.addWidget(self.lblPltStim) layHControls.addWidget(self.chkPltStim) layHControls.addStretch(1) layHControls.addWidget(self.lblStimulus) layHControls.addWidget(self.cmbStimulus) layHControls.addStretch(2) layHControls.addWidget(self.lblAmp) layHControls.addWidget(self.ledAmp) layHControls.addWidget(self.lblFreq) layHControls.addWidget(self.ledFreq) layHControls.addWidget(self.lblFreqUnit) layHControls.addStretch(10) # This widget encompasses all control subwidgets: self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # mplwidget #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkLog.clicked.connect(self.draw) self.ledNPoints.editingFinished.connect(self.draw) self.ledLogBottom.editingFinished.connect(self.draw) self.chkPltStim.clicked.connect(self.draw) self.cmbStimulus.activated.connect(self.draw) self.ledAmp.editingFinished.connect(self.draw) self.ledFreq.installEventFilter(self) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) self.draw() # initial calculation and drawing
class Plot_Phi(QWidget): # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self, parent): super(Plot_Phi, self).__init__(parent) self.data_changed = True self.view_change= True self.size_changed = True self.tool_tip = "Phase frequency response" self.tab_label = "phi(f)" self._construct_UI() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | data_changed = {1}, visible = {2}"\ .format(dict_sig, self.data_changed, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.data_changed: self.draw() self.data_changed = False self.view_changed = False self.size_changed = False elif 'view_changed' in dict_sig: self.update_view() self.view_changed = False self.size_changed = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.size_changed: self.redraw() self.size_changed = False else: if 'data_changed' in dict_sig: self.data_changed = True if 'view_changed' in dict_sig: self.view_changed = True if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.size_changed = True def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.cmbUnitsPhi = QComboBox(self) units = ["rad", "rad/pi", "deg"] scales = [1., 1./ np.pi, 180./np.pi] for unit, scale in zip(units, scales): self.cmbUnitsPhi.addItem(unit, scale) self.cmbUnitsPhi.setObjectName("cmbUnitsA") self.cmbUnitsPhi.setToolTip("Set unit for phase.") self.cmbUnitsPhi.setCurrentIndex(0) self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkWrap = QCheckBox("Wrapped Phase", self) self.chkWrap.setChecked(False) self.chkWrap.setToolTip("Plot phase wrapped to +/- pi") layHControls = QHBoxLayout() layHControls.addWidget(self.cmbUnitsPhi) layHControls.addWidget(self.chkWrap) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkWrap.clicked.connect(self.draw) self.cmbUnitsPhi.currentIndexChanged.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes - this is only called once """ if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes self.ax = self.mplwidget.fig.add_subplot(111) self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def calc_resp(self): """ (Re-)Calculate the complex frequency response H(f) """ # calculate H_cplx(W) (complex) for W = 0 ... 2 pi: self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], wholeF=True) # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans self.H_cmplx = np.nan_to_num(self.H_cmplx) #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate \|H(f)\| and draw the figure """ self.calc_resp() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ self.unitPhi = qget_cmb_box(self.cmbUnitsPhi, data=False) f_S2 = fb.fil[0]['f_S'] / 2. #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c F = self.W * f_S2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 H = np.fft.fftshift(self.H_cmplx) F -= f_S2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F H = self.H_cmplx[0:params['N_FFT']//2] F = F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated H = self.H_cmplx y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in ' if self.unitPhi == 'rad': y_str += 'rad ' + r'$\rightarrow $' scale = 1. elif self.unitPhi == 'rad/pi': y_str += 'rad' + r'$ / \pi \;\rightarrow $' scale = 1./ np.pi else: y_str += 'deg ' + r'$\rightarrow $' scale = 180./np.pi fb.fil[0]['plt_phiLabel'] = y_str fb.fil[0]['plt_phiUnit'] = self.unitPhi if self.chkWrap.isChecked(): phi_plt = np.angle(H) * scale else: phi_plt = np.unwrap(np.angle(H)) * scale #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_phi, = self.ax.plot(F, phi_plt) #--------------------------------------------------------- self.ax.set_title(r'Phase Frequency Response') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(y_str) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # Define MplWidgets: Time domain plots #---------------------------------------------------------------------- self.mplwidget_t = MplWidget(self) self.mplwidget_t.setObjectName("mplwidget_t1") self.mplwidget_t.layVMainMpl.addWidget(self.ui.wdg_ctrl_time) self.mplwidget_t.layVMainMpl.setContentsMargins(*params['wdg_margins']) # MplWidget for frequency domain plots self.mplwidget_f = MplWidget(self) self.mplwidget_f.setObjectName("mplwidget_f1") self.mplwidget_f.layVMainMpl.addWidget(self.ui.wdg_ctrl_freq) self.mplwidget_f.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # Tabbed layout with vertical tabs #---------------------------------------------------------------------- self.tabWidget = QTabWidget(self) self.tabWidget.addTab(self.mplwidget_t, "Time") self.tabWidget.addTab(self.mplwidget_f, "Frequency") # list with tabWidgets self.tab_mplwidgets = ["mplwidget_t", "mplwidget_f"] self.tabWidget.setTabPosition(QTabWidget.West) layVMain = QVBoxLayout() layVMain.addWidget(self.tabWidget) layVMain.addWidget(self.ui.wdg_ctrl_stim) layVMain.addWidget(self.ui.wdg_ctrl_run) layVMain.setContentsMargins(*params['wdg_margins'])#(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # --- run control --- self.ui.cmb_sim_select.currentIndexChanged.connect(self.fx_select) self.ui.but_run.clicked.connect(self.fx_run) self.ui.chk_fx_scale.clicked.connect(self.draw_impz_time) self.ui.chk_stim_options.clicked.connect(self._show_stim_options) # --- time domain plotting --- self.ui.cmb_plt_time_resp.currentIndexChanged.connect(self.draw_impz_time) self.ui.chk_mrk_time_resp.clicked.connect(self.draw_impz_time) self.ui.cmb_plt_time_stim.currentIndexChanged.connect(self.draw_impz_time) self.ui.chk_mrk_time_stim.clicked.connect(self.draw_impz_time) self.ui.chk_log_time.clicked.connect(self._log_mode_time) self.ui.led_log_bottom_time.editingFinished.connect(self._log_mode_time) self.ui.chk_fx_range.clicked.connect(self.draw_impz_time) self.ui.chk_win_time.clicked.connect(self.draw_impz_time) # --- frequency domain plotting --- self.ui.cmb_plt_freq_resp.currentIndexChanged.connect(self.draw_impz_freq) self.ui.chk_mrk_freq_resp.clicked.connect(self.draw_impz_freq) self.ui.cmb_plt_freq_stim.currentIndexChanged.connect(self.draw_impz_freq) self.ui.chk_mrk_freq_stim.clicked.connect(self.draw_impz_freq) self.ui.chk_log_freq.clicked.connect(self._log_mode_freq) self.ui.led_log_bottom_freq.editingFinished.connect(self._log_mode_freq) self.ui.chk_win_freq.clicked.connect(self.draw_impz_freq) # frequency widgets require special handling as they are scaled with f_s self.ui.ledFreq1.installEventFilter(self) self.ui.ledFreq2.installEventFilter(self) self.mplwidget_t.mplToolbar.sig_tx.connect(self.process_sig_rx) # connect to toolbar self.mplwidget_f.mplToolbar.sig_tx.connect(self.process_sig_rx) # connect to toolbar # When user has selected a different tab, trigger a draw (incl. maybe recalc) of current tab self.tabWidget.currentChanged.connect(self.draw) # passes number of active tab self.sig_rx.connect(self.ui.sig_rx) self.ui.sig_tx.connect(self.process_sig_rx) # connect to widgets and signals upstream
def _construct_UI(self): self.chkLog = QCheckBox("Log.", self) self.chkLog.setObjectName("chkLog") self.chkLog.setToolTip("Logarithmic scale") self.chkLog.setChecked(False) self.chkPolar = QCheckBox("Polar", self) self.chkPolar.setObjectName("chkPolar") self.chkPolar.setToolTip("Polar coordinate range") self.chkPolar.setChecked(False) self.lblBottom = QLabel("Bottom:", self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setToolTip("Minimum display value.") self.lblBottomdB = QLabel("dB", self) self.lblBottomdB.setVisible(self.chkLog.isChecked()) self.lblTop = QLabel("Top:", self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.lblTopdB = QLabel("dB", self) self.lblTopdB.setVisible(self.chkLog.isChecked()) self.chkUC = QCheckBox("UC", self) self.chkUC.setObjectName("chkUC") self.chkUC.setToolTip("Plot unit circle") self.chkUC.setChecked(True) self.chkPZ = QCheckBox("P/Z", self) self.chkPZ.setObjectName("chkPZ") self.chkPZ.setToolTip("Plot poles and zeros") self.chkPZ.setChecked(True) self.chkHf = QCheckBox("H(f)", self) self.chkHf.setObjectName("chkHf") self.chkHf.setToolTip("Plot H(f) along the unit circle") self.chkHf.setChecked(True) modes = ['None', 'Mesh', 'Surf', 'Contour'] self.cmbMode3D = QComboBox(self) self.cmbMode3D.addItems(modes) self.cmbMode3D.setObjectName("cmbShow3D") self.cmbMode3D.setToolTip("Select 3D-plot mode.") self.cmbMode3D.setCurrentIndex(0) self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkColormap_r = QCheckBox("reverse", self) self.chkColormap_r.setToolTip("reverse colormap") self.chkColormap_r.setChecked(True) self.cmbColormap = QComboBox(self) self._init_cmb_colormap() self.cmbColormap.setToolTip("Select colormap") self.chkColBar = QCheckBox("Colorbar", self) self.chkColBar.setObjectName("chkColBar") self.chkColBar.setToolTip("Show colorbar") self.chkColBar.setChecked(False) self.chkLighting = QCheckBox("Lighting", self) self.chkLighting.setObjectName("chkLighting") self.chkLighting.setToolTip("Enable light source") self.chkLighting.setChecked(False) self.lblAlpha = QLabel("Alpha", self) self.diaAlpha = QDial(self) self.diaAlpha.setRange(0., 10.) self.diaAlpha.setValue(10) self.diaAlpha.setTracking(False) # produce less events when turning self.diaAlpha.setFixedHeight(30) self.diaAlpha.setFixedWidth(30) self.diaAlpha.setWrapping(False) self.diaAlpha.setToolTip("<span>Set transparency for surf and contour plots.</span>") self.lblHatch = QLabel("Stride", self) self.diaHatch = QDial(self) self.diaHatch.setRange(0., 9.) self.diaHatch.setValue(5) self.diaHatch.setTracking(False) # produce less events when turning self.diaHatch.setFixedHeight(30) self.diaHatch.setFixedWidth(30) self.diaHatch.setWrapping(False) self.diaHatch.setToolTip("Set line density for various plots.") self.chkContour2D = QCheckBox("Contour2D", self) self.chkContour2D.setObjectName("chkContour2D") self.chkContour2D.setToolTip("Plot 2D-contours at z =0") self.chkContour2D.setChecked(False) #---------------------------------------------------------------------- # LAYOUT for UI widgets #---------------------------------------------------------------------- layGControls = QGridLayout() layGControls.addWidget(self.chkLog, 0, 0) layGControls.addWidget(self.chkPolar, 1, 0) layGControls.addWidget(self.lblTop, 0, 2) layGControls.addWidget(self.ledTop, 0, 4) layGControls.addWidget(self.lblTopdB, 0, 5) layGControls.addWidget(self.lblBottom, 1, 2) layGControls.addWidget(self.ledBottom, 1, 4) layGControls.addWidget(self.lblBottomdB, 1, 5) layGControls.setColumnStretch(5,1) layGControls.addWidget(self.chkUC, 0, 6) layGControls.addWidget(self.chkHf, 1, 6) layGControls.addWidget(self.chkPZ, 0, 8) layGControls.addWidget(self.cmbMode3D, 0, 10) layGControls.addWidget(self.chkContour2D, 1, 10) layGControls.addWidget(self.cmbColormap, 0,12,1,1) layGControls.addWidget(self.chkColormap_r, 1,12) layGControls.addWidget(self.chkLighting, 0, 14) layGControls.addWidget(self.chkColBar, 1, 14) layGControls.addWidget(self.lblAlpha, 0, 15) layGControls.addWidget(self.diaAlpha, 0, 16) layGControls.addWidget(self.lblHatch, 1, 15) layGControls.addWidget(self.diaHatch, 1, 16) # This widget encompasses all control subwidgets self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layGControls) #---------------------------------------------------------------------- # mplwidget #---------------------------------------------------------------------- # This is the plot pane widget, encompassing the other widgets self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self._init_grid() # initialize grid and do initial plot #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkLog.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.chkPolar.clicked.connect(self._init_grid) self.chkUC.clicked.connect(self.draw) self.chkHf.clicked.connect(self.draw) self.chkPZ.clicked.connect(self.draw) self.cmbMode3D.currentIndexChanged.connect(self.draw) self.chkColBar.clicked.connect(self.draw) self.cmbColormap.currentIndexChanged.connect(self.draw) self.chkColormap_r.clicked.connect(self._init_cmb_colormap) self.chkLighting.clicked.connect(self.draw) self.diaAlpha.valueChanged.connect(self.draw) self.diaHatch.valueChanged.connect(self.draw) self.chkContour2D.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkHf = QCheckBox("Show |H(f)|", self) self.chkHf.setToolTip( "<span>Display |H(f)| around unit circle.</span>") self.chkHf.setEnabled(True) self.chkHfLog = QCheckBox("Log. Scale", self) self.chkHfLog.setToolTip("<span>Log. scale for |H(f)|.</span>") self.chkHfLog.setEnabled(True) self.diaRad_Hf = QDial(self) self.diaRad_Hf.setRange(2., 10.) self.diaRad_Hf.setValue(2) self.diaRad_Hf.setTracking(False) # produce less events when turning self.diaRad_Hf.setFixedHeight(30) self.diaRad_Hf.setFixedWidth(30) self.diaRad_Hf.setWrapping(False) self.diaRad_Hf.setToolTip( "<span>Set max. radius for |H(f)| plot.</span>") self.lblRad_Hf = QLabel("Radius", self) self.chkFIR_P = QCheckBox("Plot FIR Poles", self) self.chkFIR_P.setToolTip("<span>Show FIR poles at the origin.</span>") self.chkFIR_P.setChecked(True) layHControls = QHBoxLayout() layHControls.addWidget(self.chkHf) layHControls.addWidget(self.chkHfLog) layHControls.addWidget(self.diaRad_Hf) layHControls.addWidget(self.lblRad_Hf) layHControls.addStretch(10) layHControls.addWidget(self.chkFIR_P) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw poles and zeros #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.chkHf.clicked.connect(self.draw) self.chkHfLog.clicked.connect(self.draw) self.diaRad_Hf.valueChanged.connect(self.draw) self.chkFIR_P.clicked.connect(self.draw)
def _construct_UI(self): self.but_log = PushButton("dB", checked=False) self.but_log.setObjectName("but_log") self.but_log.setToolTip("Logarithmic scale") self.but_plot_in_UC = PushButton("|z| < 1 ", checked=False) self.but_plot_in_UC.setObjectName("but_plot_in_UC") self.but_plot_in_UC.setToolTip("Only plot H(z) within the unit circle") self.lblBottom = QLabel(to_html("Bottom =", frmt='bi'), self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setToolTip("Minimum display value.") self.lblBottomdB = QLabel("dB", self) self.lblBottomdB.setVisible(self.but_log.isChecked()) self.lblTop = QLabel(to_html("Top =", frmt='bi'), self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.lblTopdB = QLabel("dB", self) self.lblTopdB.setVisible(self.but_log.isChecked()) self.plt_UC = PushButton("UC", checked=True) self.plt_UC.setObjectName("plt_UC") self.plt_UC.setToolTip("Plot unit circle") self.but_PZ = PushButton("P/Z ", checked=True) self.but_PZ.setObjectName("but_PZ") self.but_PZ.setToolTip("Plot poles and zeros") self.but_Hf = PushButton("H(f) ", checked=True) self.but_Hf.setObjectName("but_Hf") self.but_Hf.setToolTip("Plot H(f) along the unit circle") modes = ['None', 'Mesh', 'Surf', 'Contour'] self.cmbMode3D = QComboBox(self) self.cmbMode3D.addItems(modes) self.cmbMode3D.setObjectName("cmbShow3D") self.cmbMode3D.setToolTip("Select 3D-plot mode.") self.cmbMode3D.setCurrentIndex(0) self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.but_colormap_r = PushButton("reverse", checked=True) self.but_colormap_r.setObjectName("but_colormap_r") self.but_colormap_r.setToolTip("reverse colormap") self.cmbColormap = QComboBox(self) self._init_cmb_colormap(cmap_init=self.cmap_default) self.cmbColormap.setToolTip("Select colormap") self.but_colbar = PushButton("Colorbar ", checked=False) self.but_colbar.setObjectName("chkColBar") self.but_colbar.setToolTip("Show colorbar") self.but_lighting = PushButton("Lighting", checked=False) self.but_lighting.setObjectName("but_lighting") self.but_lighting.setToolTip("Enable light source") self.lblAlpha = QLabel(to_html("Alpha", frmt='bi'), self) self.diaAlpha = QDial(self) self.diaAlpha.setRange(0, 10) self.diaAlpha.setValue(10) self.diaAlpha.setTracking(False) # produce less events when turning self.diaAlpha.setFixedHeight(30) self.diaAlpha.setFixedWidth(30) self.diaAlpha.setWrapping(False) self.diaAlpha.setToolTip( "<span>Set transparency for surf and contour plots.</span>") self.lblHatch = QLabel(to_html("Stride", frmt='bi'), self) self.diaHatch = QDial(self) self.diaHatch.setRange(0, 9) self.diaHatch.setValue(5) self.diaHatch.setTracking(False) # produce less events when turning self.diaHatch.setFixedHeight(30) self.diaHatch.setFixedWidth(30) self.diaHatch.setWrapping(False) self.diaHatch.setToolTip("Set line density for various plots.") self.but_contour_2d = PushButton("Contour2D ", checked=False) self.but_contour_2d.setObjectName("chkContour2D") self.but_contour_2d.setToolTip("Plot 2D-contours at z =0") # ---------------------------------------------------------------------- # LAYOUT for UI widgets # ---------------------------------------------------------------------- layGControls = QGridLayout() layGControls.addWidget(self.but_log, 0, 0) layGControls.addWidget(self.but_plot_in_UC, 1, 0) layGControls.addWidget(self.lblTop, 0, 2) layGControls.addWidget(self.ledTop, 0, 4) layGControls.addWidget(self.lblTopdB, 0, 5) layGControls.addWidget(self.lblBottom, 1, 2) layGControls.addWidget(self.ledBottom, 1, 4) layGControls.addWidget(self.lblBottomdB, 1, 5) layGControls.setColumnStretch(5, 1) layGControls.addWidget(self.plt_UC, 0, 6) layGControls.addWidget(self.but_Hf, 1, 6) layGControls.addWidget(self.but_PZ, 0, 8) layGControls.addWidget(self.cmbMode3D, 0, 10) layGControls.addWidget(self.but_contour_2d, 1, 10) layGControls.addWidget(self.cmbColormap, 0, 12, 1, 1) layGControls.addWidget(self.but_colormap_r, 1, 12) layGControls.addWidget(self.but_lighting, 0, 14) layGControls.addWidget(self.but_colbar, 1, 14) layGControls.addWidget(self.lblAlpha, 0, 15) layGControls.addWidget(self.diaAlpha, 0, 16) layGControls.addWidget(self.lblHatch, 1, 15) layGControls.addWidget(self.diaHatch, 1, 16) # This widget encompasses all control subwidgets self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layGControls) # ---------------------------------------------------------------------- # mplwidget # ---------------------------------------------------------------------- # This is the plot pane widget, encompassing the other widgets self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_3d.html" self.setLayout(self.mplwidget.layVMainMpl) self._init_grid() # initialize grid and do initial plot # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_log.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.but_plot_in_UC.clicked.connect(self._init_grid) self.plt_UC.clicked.connect(self.draw) self.but_Hf.clicked.connect(self.draw) self.but_PZ.clicked.connect(self.draw) self.cmbMode3D.currentIndexChanged.connect(self.draw) self.but_colbar.clicked.connect(self.draw) self.cmbColormap.currentIndexChanged.connect(self.draw) self.but_colormap_r.clicked.connect(self.draw) self.but_lighting.clicked.connect(self.draw) self.diaAlpha.valueChanged.connect(self.draw) self.diaHatch.valueChanged.connect(self.draw) self.but_contour_2d.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip( "Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel("in", self) units = ['dB', 'V', 'W', 'Auto'] self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip( "<span>Set unit for y-axis:\n" "dB is attenuation (positive values), V and W are gain (less than 1).</span>" ) self.cmbUnitsA.setCurrentIndex(0) self.lbl_log_bottom = QLabel("Bottom", self) self.led_log_bottom = QLineEdit(self) self.led_log_bottom.setText(str(self.log_bottom)) self.led_log_bottom.setToolTip( "<span>Minimum display value for dB. scale.</span>") self.lbl_log_unit = QLabel("dB", self) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkZerophase = QCheckBox("Zero phase", self) self.chkZerophase.setToolTip( "<span>Remove linear phase calculated from filter order.\n" "Attention: This makes no sense for a non-linear phase system!</span>" ) self.lblInset = QLabel("Inset", self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.chkSpecs = QCheckBox("Specs", self) self.chkSpecs.setChecked(False) self.chkSpecs.setToolTip("Display filter specs as hatched regions") self.chkPhase = QCheckBox("Phase", self) self.chkPhase.setToolTip("Overlay phase") self.chkPhase.setChecked(False) self.chkAlign = QCheckBox("Align", self) self.chkAlign.setToolTip( "<span>Try to align grids for magnitude and phase " "(doesn't work in all cases).</span>") self.chkAlign.setChecked(True) self.chkAlign.setVisible(self.chkPhase.isChecked()) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmbUnitsA) layHControls.addStretch(1) layHControls.addWidget(self.lbl_log_bottom) layHControls.addWidget(self.led_log_bottom) layHControls.addWidget(self.lbl_log_unit) layHControls.addStretch(1) layHControls.addWidget(self.chkZerophase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.chkSpecs) layHControls.addStretch(1) layHControls.addWidget(self.chkPhase) layHControls.addWidget(self.chkAlign) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_hf.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self.draw) self.led_log_bottom.editingFinished.connect(self.update_view) self.cmbShowH.currentIndexChanged.connect(self.draw) self.chkZerophase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.chkSpecs.clicked.connect(self.draw) self.chkPhase.clicked.connect(self.draw) self.chkAlign.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip("Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel("in", self) units = ['dB', 'V', 'W', 'Auto'] self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip("<span>Set unit for y-axis:\n" "dB is attenuation (positive values), V and W are gain (less than 1).</span>") self.cmbUnitsA.setCurrentIndex(0) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkLinphase = QCheckBox("Zero phase", self) self.chkLinphase.setToolTip("<span>Subtract linear phase according to filter order.\n" "Attention: this makes no sense for a non-linear phase system!</span>") self.lblInset = QLabel("Inset", self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.chkSpecs = QCheckBox("Show Specs", self) self.chkSpecs.setChecked(False) self.chkSpecs.setToolTip("Display filter specs as hatched regions") self.chkPhase = QCheckBox("Phase", self) self.chkPhase.setToolTip("Overlay phase") #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmbUnitsA) layHControls.addStretch(1) layHControls.addWidget(self.chkLinphase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.chkSpecs) layHControls.addStretch(1) layHControls.addWidget(self.chkPhase) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self.draw) self.cmbShowH.currentIndexChanged.connect(self.draw) self.chkLinphase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.chkSpecs.clicked.connect(self.draw) self.chkPhase.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals)
def _construct_UI(self): self.chkLog = QCheckBox("Log.", self) self.chkLog.setObjectName("chkLog") self.chkLog.setToolTip("Logarithmic scale") self.chkLog.setChecked(False) self.chkPolar = QCheckBox("Polar", self) self.chkPolar.setObjectName("chkPolar") self.chkPolar.setToolTip("Polar coordinate range") self.chkPolar.setChecked(False) self.lblBottom = QLabel("Bottom =", self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setToolTip("Minimum display value.") self.lblTop = QLabel("Top:", self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.chkUC = QCheckBox("UC", self) self.chkUC.setObjectName("chkUC") self.chkUC.setToolTip("Plot unit circle") self.chkUC.setChecked(True) self.chkPZ = QCheckBox("P/Z", self) self.chkPZ.setObjectName("chkPZ") self.chkPZ.setToolTip("Plot poles and zeros") self.chkPZ.setChecked(True) self.chkHf = QCheckBox("H(f)", self) self.chkHf.setObjectName("chkHf") self.chkHf.setToolTip("Plot H(f) along the unit circle") self.chkHf.setChecked(True) modes = ['None', 'Mesh', 'Surf', 'Contour'] self.cmbMode3D = QComboBox(self) self.cmbMode3D.addItems(modes) self.cmbMode3D.setObjectName("cmbShow3D") self.cmbMode3D.setToolTip("Select 3D-plot mode.") self.cmbMode3D.setCurrentIndex(0) self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkColormap_r = QCheckBox("reverse", self) self.chkColormap_r.setToolTip("reverse colormap") self.chkColormap_r.setChecked(True) self.cmbColormap = QComboBox(self) self._init_cmb_colormap() self.cmbColormap.setToolTip("Select colormap") self.chkColBar = QCheckBox("Colorbar", self) self.chkColBar.setObjectName("chkColBar") self.chkColBar.setToolTip("Show colorbar") self.chkColBar.setChecked(False) self.chkLighting = QCheckBox("Lighting", self) self.chkLighting.setObjectName("chkLighting") self.chkLighting.setToolTip("Enable light source") self.chkLighting.setChecked(False) self.lblAlpha = QLabel("Alpha", self) self.diaAlpha = QDial(self) self.diaAlpha.setRange(0., 10.) self.diaAlpha.setValue(10) self.diaAlpha.setTracking(False) # produce less events when turning self.diaAlpha.setFixedHeight(30) self.diaAlpha.setFixedWidth(30) self.diaAlpha.setWrapping(False) self.diaAlpha.setToolTip( "<span>Set transparency for surf and contour plots.</span>") self.lblHatch = QLabel("Stride", self) self.diaHatch = QDial(self) self.diaHatch.setRange(0., 9.) self.diaHatch.setValue(5) self.diaHatch.setTracking(False) # produce less events when turning self.diaHatch.setFixedHeight(30) self.diaHatch.setFixedWidth(30) self.diaHatch.setWrapping(False) self.diaHatch.setToolTip("Set line density for various plots.") self.chkContour2D = QCheckBox("Contour2D", self) self.chkContour2D.setObjectName("chkContour2D") self.chkContour2D.setToolTip("Plot 2D-contours at z =0") self.chkContour2D.setChecked(False) #---------------------------------------------------------------------- # LAYOUT for UI widgets #---------------------------------------------------------------------- layGControls = QGridLayout() layGControls.addWidget(self.chkLog, 0, 0) layGControls.addWidget(self.chkPolar, 1, 0) layGControls.addWidget(self.lblTop, 0, 2) layGControls.addWidget(self.lblBottom, 1, 2) layGControls.addWidget(self.ledTop, 0, 4) layGControls.addWidget(self.ledBottom, 1, 4) layGControls.setColumnStretch(5, 1) layGControls.addWidget(self.chkUC, 0, 6) layGControls.addWidget(self.chkHf, 1, 6) layGControls.addWidget(self.chkPZ, 0, 8) layGControls.addWidget(self.cmbMode3D, 0, 10) layGControls.addWidget(self.chkContour2D, 1, 10) layGControls.addWidget(self.cmbColormap, 0, 12, 1, 1) layGControls.addWidget(self.chkColormap_r, 1, 12) layGControls.addWidget(self.chkLighting, 0, 14) layGControls.addWidget(self.chkColBar, 1, 14) layGControls.addWidget(self.lblAlpha, 0, 15) layGControls.addWidget(self.diaAlpha, 0, 16) layGControls.addWidget(self.lblHatch, 1, 15) layGControls.addWidget(self.diaHatch, 1, 16) # This widget encompasses all control subwidgets self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layGControls) #---------------------------------------------------------------------- # mplwidget #---------------------------------------------------------------------- # This is the plot pane widget, encompassing the other widgets self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self._init_grid() # initialize grid and do initial plot #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkLog.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.chkPolar.clicked.connect(self._init_grid) self.chkUC.clicked.connect(self.draw) self.chkHf.clicked.connect(self.draw) self.chkPZ.clicked.connect(self.draw) self.cmbMode3D.currentIndexChanged.connect(self.draw) self.chkColBar.clicked.connect(self.draw) self.cmbColormap.currentIndexChanged.connect(self.draw) self.chkColormap_r.clicked.connect(self._init_cmb_colormap) self.chkLighting.clicked.connect(self.draw) self.diaAlpha.valueChanged.connect(self.draw) self.diaHatch.valueChanged.connect(self.draw) self.chkContour2D.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) self.mplwidget.mplToolbar.enable_plot(state=False) # disable initially
class Plot_FFT_win(QMainWindow): """ Create a pop-up widget for displaying time and frequency view of an FFT window. Data is passed via the dictionary `win_dict` that is passed during construction. """ # incoming sig_rx = pyqtSignal(object) # outgoing sig_tx = pyqtSignal(object) def __init__(self, parent, win_dict_name="win_fft", sym=True): super(Plot_FFT_win, self).__init__(parent) self.needs_calc = True self.needs_draw = True self.bottom_f = -80 # min. value for dB display self.bottom_t = -60 self.N = 128 # initial number of data points self.pad = 8 # amount of zero padding self.win_dict = fb.fil[0][win_dict_name] self.sym = sym self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle('pyFDA Window Viewer') self._construct_UI() qwindow_stay_on_top(self, True) #------------------------------------------------------------------------------ def closeEvent(self, event): """ Catch closeEvent (user has tried to close the window) and send a signal to parent where window closing is registered before actually closing the window. """ self.sig_tx.emit({'sender': __name__, 'closeEvent': ''}) event.accept() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | visible = {1}"\ .format(dict_sig, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False self.needs_draw = False elif 'view_changed' in dict_sig or self.needs_draw: self.update_view() self.needs_draw = False elif ('ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized')\ or self.needs_redraw: self.redraw() else: if 'data_changed' in dict_sig: self.needs_calc = True elif 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig[ 'ui_changed'] == 'resized': self.needs_redraw = True def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chk_auto_N = QCheckBox(self) self.chk_auto_N.setChecked(False) self.chk_auto_N.setToolTip( "Use number of points from calling routine.") self.lbl_auto_N = QLabel("Auto " + to_html("N", frmt='i')) self.led_N = QLineEdit(self) self.led_N.setText(str(self.N)) self.led_N.setMaximumWidth(70) self.led_N.setToolTip("<span>Number of window data points.</span>") self.chk_log_t = QCheckBox("Log", self) self.chk_log_t.setChecked(False) self.chk_log_t.setToolTip("Display in dB") self.led_log_bottom_t = QLineEdit(self) self.led_log_bottom_t.setText(str(self.bottom_t)) self.led_log_bottom_t.setMaximumWidth(50) self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_t.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_t = QLabel("dB", self) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.chk_norm_f = QCheckBox("Norm", self) self.chk_norm_f.setChecked(True) self.chk_norm_f.setToolTip( "Normalize window spectrum for a maximum of 1.") self.chk_half_f = QCheckBox("Half", self) self.chk_half_f.setChecked(True) self.chk_half_f.setToolTip( "Display window spectrum in the range 0 ... 0.5 f_S.") self.chk_log_f = QCheckBox("Log", self) self.chk_log_f.setChecked(True) self.chk_log_f.setToolTip("Display in dB") self.led_log_bottom_f = QLineEdit(self) self.led_log_bottom_f.setText(str(self.bottom_f)) self.led_log_bottom_f.setMaximumWidth(50) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.led_log_bottom_f.setToolTip( "<span>Minimum display value for log. scale.</span>") self.lbl_log_bottom_f = QLabel("dB", self) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) layHControls = QHBoxLayout() layHControls.addWidget(self.chk_auto_N) layHControls.addWidget(self.lbl_auto_N) layHControls.addWidget(self.led_N) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_t) layHControls.addWidget(self.led_log_bottom_t) layHControls.addWidget(self.lbl_log_bottom_t) layHControls.addStretch(10) layHControls.addWidget(self.chk_norm_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_half_f) layHControls.addStretch(1) layHControls.addWidget(self.chk_log_f) layHControls.addWidget(self.led_log_bottom_f) layHControls.addWidget(self.lbl_log_bottom_f) # self.tblFiltPerf = QTableWidget(self) # self.tblFiltPerf.setAlternatingRowColors(True) # # self.tblFiltPerf.verticalHeader().setVisible(False) # self.tblFiltPerf.horizontalHeader().setHighlightSections(False) # self.tblFiltPerf.horizontalHeader().setFont(bfont) # self.tblFiltPerf.verticalHeader().setHighlightSections(False) # self.tblFiltPerf.verticalHeader().setFont(bfont) self.txtInfoBox = QTextBrowser(self) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget: Layout layVMainMpl (VBox) is defined with MplWidget, # additional widgets can be added (like self.frmControls) # The widget encompasses all other widgets. #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # ### splitter ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.mplwidget) splitter.addWidget(self.txtInfoBox) # setSizes uses absolute pixel values, but can be "misused" by specifying values # that are way too large: in this case, the space is distributed according # to the _ratio_ of the values: splitter.setSizes([3000, 1000]) self.setCentralWidget(splitter) #self.setCentralWidget(self.mplwidget) #---------------------------------------------------------------------- # Set subplots # self.ax = self.mplwidget.fig.subplots(nrows=1, ncols=2) self.ax_t = self.ax[0] self.ax_f = self.ax[1] self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chk_log_f.clicked.connect(self.update_view) self.chk_log_t.clicked.connect(self.update_view) self.led_log_bottom_t.editingFinished.connect(self.update_bottom) self.led_log_bottom_f.editingFinished.connect(self.update_bottom) self.chk_auto_N.clicked.connect(self.draw) self.led_N.editingFinished.connect(self.draw) self.chk_norm_f.clicked.connect(self.draw) self.chk_half_f.clicked.connect(self.update_view) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------ def update_bottom(self): """ Update log bottom settings """ self.bottom_t = safe_eval(self.led_log_bottom_t.text(), self.bottom_t, sign='neg', return_type='float') self.led_log_bottom_t.setText(str(self.bottom_t)) self.bottom_f = safe_eval(self.led_log_bottom_f.text(), self.bottom_f, sign='neg', return_type='float') self.led_log_bottom_f.setText(str(self.bottom_f)) self.update_view() #------------------------------------------------------------------------------ def calc_win(self): """ (Re-)Calculate the window and its FFT """ self.led_N.setEnabled(not self.chk_auto_N.isChecked()) if self.chk_auto_N.isChecked(): self.N = self.win_dict['win_len'] self.led_N.setText(str(self.N)) else: self.N = safe_eval(self.led_N.text(), self.N, sign='pos', return_type='int') self.n = np.arange(self.N) self.win = calc_window_function(self.win_dict, self.win_dict['name'], self.N, sym=self.sym) self.nenbw = self.N * np.sum(np.square(self.win)) / (np.square( np.sum(self.win))) self.scale = self.N / np.sum(self.win) self.F = fftfreq(self.N * self.pad, d=1. / fb.fil[0]['f_S']) # use zero padding self.Win = np.abs(fft(self.win, self.N * self.pad)) if self.chk_norm_f.isChecked(): self.Win /= (self.N / self.scale ) # correct gain for periodic signals (coherent gain) #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate \|H(f)\| and draw the figure """ self.calc_win() self.update_view() self.update_info() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ self.ax_t.cla() self.ax_f.cla() self.ax_t.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_t.set_ylabel(r'$w[n] \; \rightarrow$') self.ax_f.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f.set_ylabel(r'$W(f) \; \rightarrow$') if self.chk_log_t.isChecked(): self.ax_t.plot( self.n, np.maximum(20 * np.log10(np.abs(self.win)), self.bottom_t)) else: self.ax_t.plot(self.n, self.win) if self.chk_half_f.isChecked(): F = self.F[:len(self.F * self.pad) // 2] Win = self.Win[:len(self.F * self.pad) // 2] else: F = fftshift(self.F) Win = fftshift(self.Win) if self.chk_log_f.isChecked(): self.ax_f.plot( F, np.maximum(20 * np.log10(np.abs(Win)), self.bottom_f)) nenbw = 10 * np.log10(self.nenbw) unit_nenbw = "dB" else: self.ax_f.plot(F, Win) nenbw = self.nenbw unit_nenbw = "bins" self.led_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.lbl_log_bottom_t.setEnabled(self.chk_log_t.isChecked()) self.led_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) self.lbl_log_bottom_f.setEnabled(self.chk_log_f.isChecked()) window_name = self.win_dict['name'] param_txt = "" if self.win_dict['n_par'] > 0: param_txt = " (" + self.win_dict['par'][1][ 0] + " = {0:.3g})".format(self.win_dict['par'][2][0]) if self.win_dict['n_par'] > 1: param_txt = param_txt[:-1]\ + ", {0:s} = {1:.3g})".format(self.win_dict['par'][1][1], self.win_dict['par'][2][1]) self.mplwidget.fig.suptitle(r'{0} Window'.format(window_name) + param_txt) # create two empty patches handles = [ mpl_patches.Rectangle( (0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0) ] * 2 labels = [] labels.append("$NENBW$ = {0:.4g} {1}".format(nenbw, unit_nenbw)) labels.append("$CGAIN$ = {0:.4g}".format(self.scale)) self.ax_f.legend(handles, labels, loc='best', fontsize='small', fancybox=True, framealpha=0.7, handlelength=0, handletextpad=0) self.redraw() #------------------------------------------------------------------------------ def update_info(self): """ Update the text info box for the window """ if 'info' in self.win_dict: self.txtInfoBox.setText(self.win_dict['info']) #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
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()
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_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkWarnings = QCheckBox(self.tr("Verbose"), self) self.chkWarnings.setChecked(self.verbose) self.chkWarnings.setToolTip(self.tr("<span>Print messages about singular group delay" "and calculation times.")) self.cmbAlgorithm = QComboBox(self) for t in [(self.tr("Auto"),"auto"),(self.tr("Scipy"),"scipy"), (self.tr("JOS"), "jos"), (self.tr("Diff"), "diff"), (self.tr("Shpak"), "shpak")]: # text, data self.cmbAlgorithm.addItem(*t) qset_cmb_box(self.cmbAlgorithm, self.algorithm,data=True) self.cmbAlgorithm.setToolTip(self.tr("<span>Select algorithm for calculating " "the group delay.</span>")) self.cmbAlgorithm.setItemData(0, self.tr("<span>Try to find best-suited algorithm." "</span>"),Qt.ToolTipRole) self.cmbAlgorithm.setItemData(1, self.tr("<span>Scipy algorithm.</span>"),Qt.ToolTipRole) self.cmbAlgorithm.setItemData(2, self.tr("<span>J.O. Smith's algorithm.</span>"),Qt.ToolTipRole) self.cmbAlgorithm.setItemData(3, self.tr("<span>Textbook-style, differentiate " "the phase.</span>"),Qt.ToolTipRole) self.cmbAlgorithm.setItemData(4, self.tr("<span>Shpak's algorithm for SOS and other " "IIR filters.</span>"),Qt.ToolTipRole) #self.chkScipy = QCheckBox("Scipy", self) #self.chkScipy.setChecked(False) #self.chkScipy.setToolTip("Use scipy group delay routine") layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.chkWarnings) #layHControls.addWidget(self.chkScipy) layHControls.addWidget(self.cmbAlgorithm) # This widget encompasses all control subwidgets: self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.cmbAlgorithm.currentIndexChanged.connect(self.draw)
class Plot_Tau_G(QWidget): """ Widget for plotting the group delay """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self, parent): super(Plot_Tau_G, self).__init__(parent) self.verbose = False # suppress warnings self.needs_draw = True # flag whether plot needs to be updated self.needs_redraw = True # flag whether plot needs to be redrawn self.tool_tip = "Group delay" self.tab_label = "tau_g" self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements (currently commented out) """ # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # # ============================================================================= self.mplwidget = MplWidget(self) # self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) #------------------------------------------------------------------------------ def process_signals(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_draw, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_draw: self.draw() self.needs_draw = False self.needs_redraw = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.needs_redraw: self.redraw() self.needs_redraw = False elif 'view_changed' in dict_sig: self.update_view() else: if 'data_changed' in dict_sig or 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig[ 'ui_changed'] == 'resized': self.needs_redraw = True #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes """ self.ax = self.mplwidget.fig.add_subplot(111) self.ax.clear() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def calc_tau_g(self): """ (Re-)Calculate the complex frequency response H(f) """ bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: self.W, self.tau_g = grpdelay( bb, aa, params['N_FFT'], whole=True, verbose=self.verbose) # self.chkWarnings.isChecked()) # Zero phase filters have no group delay (Causal+AntiCausal) if 'baA' in fb.fil[0]: self.tau_g = np.zeros(self.tau_g.size) #------------------------------------------------------------------------------ def draw(self): self.calc_tau_g() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c f_S2 = fb.fil[0]['f_S'] / 2. F = self.W * f_S2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift tau_g and F by f_S/2 tau_g = np.fft.fftshift(self.tau_g) F -= f_S2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F tau_g = self.tau_g[0:params['N_FFT'] // 2] F = F[0:params['N_FFT'] // 2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated tau_g = self.tau_g #================ Main Plotting Routine ========================= #=== clear the axes and (re)draw the plot if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $' else: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\ + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $' tau_g = tau_g / fb.fil[0]['f_S'] #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_tau_g, = self.ax.plot(F, tau_g, label="Group Delay") #--------------------------------------------------------- self.ax.set_title(r'Group Delay $ \tau_g$') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(tau_str) # widen y-limits to suppress numerical inaccuracies when tau_g = constant self.ax.set_ylim([max(min(tau_g) - 0.5, 0), max(tau_g) + 0.5]) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
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_Hf(QWidget): """ Widget for plotting \|H(f)\|, frequency specs and the phase """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self, parent): super(Plot_Hf, self).__init__(parent) self.needs_draw = True # flag whether plot needs to be updated self.needs_redraw = True # flag whether plot needs to be redrawn self.tool_tip = "Magnitude and phase frequency response" self.tab_label = "|H(f)|" self._construct_ui() def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip("Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel("in", self) units = ['dB', 'V', 'W', 'Auto'] self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip("<span>Set unit for y-axis:\n" "dB is attenuation (positive values), V and W are gain (less than 1).</span>") self.cmbUnitsA.setCurrentIndex(0) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkLinphase = QCheckBox("Zero phase", self) self.chkLinphase.setToolTip("<span>Subtract linear phase according to filter order.\n" "Attention: this makes no sense for a non-linear phase system!</span>") self.lblInset = QLabel("Inset", self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.chkSpecs = QCheckBox("Show Specs", self) self.chkSpecs.setChecked(False) self.chkSpecs.setToolTip("Display filter specs as hatched regions") self.chkPhase = QCheckBox("Phase", self) self.chkPhase.setToolTip("Overlay phase") #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmbUnitsA) layHControls.addStretch(1) layHControls.addWidget(self.chkLinphase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.chkSpecs) layHControls.addStretch(1) layHControls.addWidget(self.chkPhase) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self.draw) self.cmbShowH.currentIndexChanged.connect(self.draw) self.chkLinphase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.chkSpecs.clicked.connect(self.draw) self.chkPhase.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) #------------------------------------------------------------------------------ def process_signals(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_draw, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\ or 'home' in dict_sig or self.needs_draw: self.draw() self.needs_draw = False self.needs_redraw = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.needs_redraw: self.redraw() self.needs_redraw = False elif 'view_changed' in dict_sig: self.update_view() else: # TODO: draw wouldn't be necessary for 'view_changed', only update view if 'data_changed' in dict_sig or 'specs_changed' in dict_sig or 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.needs_redraw = True #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes """ # self.ax = self.mplwidget.ax self.ax = self.mplwidget.fig.add_subplot(111) self.ax.clear() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def plot_spec_limits(self, ax): """ Plot the specifications limits (F_SB, A_SB, ...) as hatched areas with borders. """ hatch = params['mpl_hatch'] hatch_borders = params['mpl_hatch_border'] def dB(lin): return 20 * np.log10(lin) def _plot_specs(): # upper limits: ax.plot(F_lim_upl, A_lim_upl, F_lim_upc, A_lim_upc, F_lim_upr, A_lim_upr, **hatch_borders) if A_lim_upl: ax.fill_between(F_lim_upl, max(A_lim_upl), A_lim_upl, **hatch) if A_lim_upc: ax.fill_between(F_lim_upc, max(A_lim_upc), A_lim_upc, **hatch) if A_lim_upr: ax.fill_between(F_lim_upr, max(A_lim_upr), A_lim_upr, **hatch) # lower limits: ax.plot(F_lim_lol, A_lim_lol, F_lim_loc, A_lim_loc, F_lim_lor, A_lim_lor, **hatch_borders) if A_lim_lol: ax.fill_between(F_lim_lol, min(A_lim_lol), A_lim_lol, **hatch) if A_lim_loc: ax.fill_between(F_lim_loc, min(A_lim_loc), A_lim_loc, **hatch) if A_lim_lor: ax.fill_between(F_lim_lor, min(A_lim_lor), A_lim_lor, **hatch) if self.unitA == 'V': exp = 1. elif self.unitA == 'W': exp = 2. if self.unitA == 'dB': if fb.fil[0]['ft'] == "FIR": A_PB_max = dB(1 + self.A_PB) A_PB2_max = dB(1 + self.A_PB2) else: # IIR dB A_PB_max = A_PB2_max = 0 A_PB_min = dB(1 - self.A_PB) A_PB2_min = dB(1 - self.A_PB2) A_PB_minx = min(A_PB_min, A_PB2_min) - 5 A_PB_maxx = max(A_PB_max, A_PB2_max) + 5 A_SB = dB(self.A_SB) A_SB2 = dB(self.A_SB2) A_SB_maxx = max(A_SB, A_SB2) + 10 else: # 'V' or 'W' if fb.fil[0]['ft'] == "FIR": A_PB_max = (1 + self.A_PB)**exp A_PB2_max = (1 + self.A_PB2)**exp else: # IIR lin A_PB_max = A_PB2_max = 1 A_PB_min = (1 - self.A_PB)**exp A_PB2_min = (1 - self.A_PB2)**exp A_PB_minx = min(A_PB_min, A_PB2_min) / 1.05 A_PB_maxx = max(A_PB_max, A_PB2_max) * 1.05 A_SB = self.A_SB ** exp A_SB2 = self.A_SB2 ** exp A_SB_maxx = A_PB_min / 10. F_max = self.f_S/2 F_PB = self.F_PB F_SB = fb.fil[0]['F_SB'] * self.f_S F_SB2 = fb.fil[0]['F_SB2'] * self.f_S F_PB2 = fb.fil[0]['F_PB2'] * self.f_S F_lim_upl = F_lim_lol = [] # left side limits, lower and upper A_lim_upl = A_lim_lol = [] F_lim_upc = F_lim_loc = [] # center limits, lower and upper A_lim_upc = A_lim_loc = [] F_lim_upr = F_lim_lor = [] # right side limits, lower and upper A_lim_upr = A_lim_lor = [] if fb.fil[0]['rt'] == 'LP': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB, F_SB, F_max] A_lim_upr = [A_SB_maxx, A_SB, A_SB] if fb.fil[0]['rt'] == 'HP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB, F_PB, F_max] A_lim_upr = [A_PB_maxx, A_PB_max, A_PB_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB_min, A_PB_min] if fb.fil[0]['rt'] == 'BS': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upc = [F_SB, F_SB, F_SB2, F_SB2] A_lim_upc = [A_SB_maxx, A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB2, F_PB2, F_max] A_lim_upr = [A_PB_maxx, A_PB2_max, A_PB2_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB2_min, A_PB2_min] if fb.fil[0]['rt'] == 'BP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB2, F_SB2, F_max] A_lim_upr = [A_SB_maxx, A_SB2, A_SB2] if fb.fil[0]['rt'] == 'HIL': F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = np.array(F_lim_upr) F_lim_lor = np.array(F_lim_lor) F_lim_upl = np.array(F_lim_upl) F_lim_lol = np.array(F_lim_lol) F_lim_upc = np.array(F_lim_upc) F_lim_loc = np.array(F_lim_loc) _plot_specs() # plot specs in the range 0 ... f_S/2 if fb.fil[0]['freqSpecsRangeType'] != 'half': # add plot limits for other half of the spectrum if fb.fil[0]['freqSpecsRangeType'] == 'sym': # frequency axis +/- f_S/2 F_lim_upl = -F_lim_upl F_lim_lol = -F_lim_lol F_lim_upc = -F_lim_upc F_lim_loc = -F_lim_loc F_lim_upr = -F_lim_upr F_lim_lor = -F_lim_lor else: # -> 'whole' F_lim_upl = self.f_S - F_lim_upl F_lim_lol = self.f_S - F_lim_lol F_lim_upc = self.f_S - F_lim_upc F_lim_loc = self.f_S - F_lim_loc F_lim_upr = self.f_S - F_lim_upr F_lim_lor = self.f_S - F_lim_lor _plot_specs() #------------------------------------------------------------------------------ def draw_inset(self): """ Construct / destruct second axes for an inset second plot """ # TODO: try ax1 = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6 # TODO: choose size & position of inset, maybe dependent on filter type # or specs (i.e. where is passband etc.) # DEBUG # print(self.cmbInset.currentIndex(), self.mplwidget.fig.axes) # list of axes in Figure # for ax in self.mplwidget.fig.axes: # print(ax) # print("cmbInset, inset_idx:",self.cmbInset.currentIndex(), self.inset_idx) if self.cmbInset.currentIndex() > 0: if self.inset_idx == 0: # Inset was turned off before, create a new one # Add an axes at position rect [left, bottom, width, height]: self.ax_i = self.mplwidget.fig.add_axes([0.65, 0.61, .3, .3]) self.ax_i.clear() # clear old plot and specs # draw an opaque background with the extent of the inset plot: # self.ax_i.patch.set_facecolor('green') # without label area # self.mplwidget.fig.patch.set_facecolor('green') # whole figure extent = self.mplwidget.get_full_extent(self.ax_i, pad = 0.0) # Transform this back to figure coordinates - otherwise, it # won't behave correctly when the size of the plot is changed: extent = extent.transformed(self.mplwidget.fig.transFigure.inverted()) rect = Rectangle((extent.xmin, extent.ymin), extent.width, extent.height, facecolor=rcParams['figure.facecolor'], edgecolor='none', transform=self.mplwidget.fig.transFigure, zorder=-1) self.ax_i.patches.append(rect) self.ax_i.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_i.plot(self.F, self.H_plt) if self.cmbInset.currentIndex() == 1: # edit / navigate inset self.ax_i.set_navigate(True) self.ax.set_navigate(False) if self.specs: self.plot_spec_limits(self.ax_i) else: # edit / navigate main plot self.ax_i.set_navigate(False) self.ax.set_navigate(True) else: # inset has been turned off, delete it self.ax.set_navigate(True) try: #remove ax_i from the figure self.mplwidget.fig.delaxes(self.ax_i) except AttributeError: pass self.inset_idx = self.cmbInset.currentIndex() # update index self.draw() #------------------------------------------------------------------------------ def draw_phase(self, ax): """ Draw phase on second y-axis in the axes system passed as the argument """ try: self.mplwidget.fig.delaxes(self.ax_p) except (KeyError, AttributeError): pass if self.chkPhase.isChecked(): self.ax_p = ax.twinx() # second axes system with same x-axis for phase self.ax_p.is_twin = True # mark this as 'twin' to suppress second grid in mpl_widget # phi_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$' if fb.fil[0]['plt_phiUnit'] == 'rad': phi_str += ' in rad ' + r'$\rightarrow $' scale = 1. elif fb.fil[0]['plt_phiUnit'] == 'rad/pi': phi_str += ' in rad' + r'$ / \pi \;\rightarrow $' scale = 1./ np.pi else: phi_str += ' in deg ' + r'$\rightarrow $' scale = 180./np.pi # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans phi = np.angle(np.nan_to_num(self.H_c)) #----------------------------------------------------------- self.ax_p.plot(self.F,np.unwrap(phi)*scale, 'g-.', label = "Phase") #----------------------------------------------------------- self.ax_p.set_ylabel(phi_str) nbins = len(self.ax.get_yticks()) # number of ticks on main y-axis # http://stackoverflow.com/questions/28692608/align-grid-lines-on-two-plots # http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # http://stackoverflow.com/questions/20243683/matplotlib-align-twinx-tick-marks # manual setting: #self.ax_p.set_yticks( np.linspace(self.ax_p.get_ylim()[0],self.ax_p.get_ylim()[1],nbins) ) #ax1.set_yticks(np.linspace(ax1.get_ybound()[0], ax1.get_ybound()[1], 5)) #ax2.set_yticks(np.linspace(ax2.get_ybound()[0], ax2.get_ybound()[1], 5)) #http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # use helper functions from matplotlib.ticker: # MaxNLocator: set no more than nbins + 1 ticks #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.MaxNLocator(nbins = nbins) ) # further options: integer = False, # prune = [‘lower’ | ‘upper’ | ‘both’ | None] Remove edge ticks # AutoLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.AutoLocator() ) # LinearLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.LinearLocator(numticks = nbins -1 ) ) # self.ax_p.locator_params(axis = 'y', nbins = nbins) # # self.ax_p.set_yticks(np.linspace(self.ax_p.get_ybound()[0], # self.ax_p.get_ybound()[1], # len(self.ax.get_yticks())-1)) #N = source_ax.xaxis.get_major_ticks() #target_ax.xaxis.set_major_locator(LinearLocator(N)) # else: # try: # self.mplwidget.fig.delaxes(self.ax_p) # except (KeyError, AttributeError): # pass # self.draw() #------------------------------------------------------------------------------ def calc_hf(self): """ (Re-)Calculate the complex frequency response H(f) """ # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], True) #------------------------------------------------------------------------------ def draw(self): """ Re-calculate \|H(f)\| and draw the figure """ self.calc_hf() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ # Get corners for spec display from the parameters of the target specs subwidget try: param_list = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']]\ [fb.fil[0]['fc']][fb.fil[0]['fo']]['tspecs'][1]['amp'] except KeyError: param_list = [] SB = [l for l in param_list if 'A_SB' in l] PB = [l for l in param_list if 'A_PB' in l] if SB: A_min = min([fb.fil[0][l] for l in SB]) else: A_min = 5e-4 if PB: A_max = max([fb.fil[0][l] for l in PB]) else: A_max = 1 if np.all(self.W) is None: # H(f) has not been calculated yet self.calc_hf() if self.cmbUnitsA.currentText() == 'Auto': self.unitA = fb.fil[0]['amp_specs_unit'] else: self.unitA = self.cmbUnitsA.currentText() # Linphase settings only makes sense for amplitude plot self.chkLinphase.setCheckable(self.unitA == 'V') self.chkLinphase.setEnabled(self.unitA == 'V') self.specs = self.chkSpecs.isChecked() self.linphase = self.chkLinphase.isChecked() self.f_S = fb.fil[0]['f_S'] self.F_PB = fb.fil[0]['F_PB'] * self.f_S self.F_SB = fb.fil[0]['F_SB'] * self.f_S self.A_PB = fb.fil[0]['A_PB'] self.A_PB2 = fb.fil[0]['A_PB2'] self.A_SB = fb.fil[0]['A_SB'] self.A_SB2 = fb.fil[0]['A_SB2'] f_lim = fb.fil[0]['freqSpecsRange'] #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c self.F = self.W / (2 * np.pi) * self.f_S if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 self.H_c = np.fft.fftshift(self.H_cmplx) self.F -= self.f_S/2. elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F self.H_c = self.H_cmplx[0:params['N_FFT']//2] self.F = self.F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated self.H_c = self.H_cmplx # now calculate mag / real / imaginary part of H_c: if self.linphase: # remove the linear phase self.H_c = self.H_c * np.exp(1j * self.W[0:len(self.F)] * fb.fil[0]["N"]/2.) if self.cmbShowH.currentIndex() == 0: # show magnitude of H H = abs(self.H_c) H_str = r'$|H(\mathrm{e}^{\mathrm{j} \Omega})|$' elif self.cmbShowH.currentIndex() == 1: # show real part of H H = self.H_c.real H_str = r'$\Re \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' else: # show imag. part of H H = self.H_c.imag H_str = r'$\Im \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' #================ Main Plotting Routine ========================= #=== clear the axes and (re)draw the plot (if selectable) if self.ax.get_navigate(): if self.unitA == 'dB': A_lim = [20*np.log10(A_min) -10, 20*np.log10(1+A_max) +1] self.H_plt = 20*np.log10(abs(H)) H_str += ' in dB ' + r'$\rightarrow$' elif self.unitA == 'V': # 'lin' A_lim = [0, (1.05 + A_max)] self.H_plt = H H_str +=' in V ' + r'$\rightarrow $' self.ax.axhline(linewidth=1, color='k') # horizontal line at 0 else: # unit is W A_lim = [0, (1.03 + A_max)**2.] self.H_plt = H * H.conj() H_str += ' in W ' + r'$\rightarrow $' #----------------------------------------------------------- self.ax.clear() self.ax.plot(self.F, self.H_plt, label = 'H(f)') # TODO: self.draw_inset() # this gives an infinite recursion self.draw_phase(self.ax) #----------------------------------------------------------- #============= Set Limits and draw specs ========================= if self.specs: self.plot_spec_limits(self.ax) # self.ax_bounds = [self.ax.get_ybound()[0], self.ax.get_ybound()[1]]#, self.ax.get] self.ax.set_xlim(f_lim) self.ax.set_ylim(A_lim) self.ax.set_title(r'Magnitude Frequency Response') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(H_str) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class Plot_Impz(QWidget): """ Construct a widget for plotting impulse and general transient responses """ # incoming sig_rx = pyqtSignal(object) # outgoing, e.g. when stimulus has been calculated sig_tx = pyqtSignal(object) def __init__(self, parent): super(Plot_Impz, self).__init__(parent) self.ACTIVE_3D = False self.ui = PlotImpz_UI(self) # create the UI part with buttons etc. # initial settings for line edit widgets self.f1 = self.ui.f1 self.f2 = self.ui.f2 self.bottom_t = self.ui.bottom_t self.bottom_f = self.ui.bottom_f self.needs_draw = True # flag whether plots need to be updated self.needs_redraw = [True] * 2 # flag which plot needs to be redrawn self.fx_sim = False # initial setting for fixpoint simulation self.tool_tip = "Impulse and transient response" self.tab_label = "h[n]" self.active_tab = 0 # index for active tab self._construct_UI() #-------------------------------------------- # initialize routines and settings self._log_mode_time() self._log_mode_freq() self.fx_select() # initialize fixpoint or float simulation self.draw() # initial calculation and drawing def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # Define MplWidgets: Time domain plots #---------------------------------------------------------------------- self.mplwidget_t = MplWidget(self) self.mplwidget_t.setObjectName("mplwidget_t1") self.mplwidget_t.layVMainMpl.addWidget(self.ui.wdg_ctrl_time) self.mplwidget_t.layVMainMpl.setContentsMargins(*params['wdg_margins']) # MplWidget for frequency domain plots self.mplwidget_f = MplWidget(self) self.mplwidget_f.setObjectName("mplwidget_f1") self.mplwidget_f.layVMainMpl.addWidget(self.ui.wdg_ctrl_freq) self.mplwidget_f.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # Tabbed layout with vertical tabs #---------------------------------------------------------------------- self.tabWidget = QTabWidget(self) self.tabWidget.addTab(self.mplwidget_t, "Time") self.tabWidget.addTab(self.mplwidget_f, "Frequency") # list with tabWidgets self.tab_mplwidgets = ["mplwidget_t", "mplwidget_f"] self.tabWidget.setTabPosition(QTabWidget.West) layVMain = QVBoxLayout() layVMain.addWidget(self.tabWidget) layVMain.addWidget(self.ui.wdg_ctrl_stim) layVMain.addWidget(self.ui.wdg_ctrl_run) layVMain.setContentsMargins( *params['wdg_margins']) #(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # --- run control --- self.ui.cmb_sim_select.currentIndexChanged.connect(self.fx_select) self.ui.but_run.clicked.connect(self.fx_run) self.ui.chk_fx_scale.clicked.connect(self.draw_impz_time) self.ui.chk_stim_options.clicked.connect(self._show_stim_options) # --- time domain plotting --- self.ui.cmb_plt_time_resp.currentIndexChanged.connect( self.draw_impz_time) self.ui.chk_mrk_time_resp.clicked.connect(self.draw_impz_time) self.ui.cmb_plt_time_stim.currentIndexChanged.connect( self.draw_impz_time) self.ui.chk_mrk_time_stim.clicked.connect(self.draw_impz_time) self.ui.chk_log_time.clicked.connect(self._log_mode_time) self.ui.led_log_bottom_time.editingFinished.connect( self._log_mode_time) self.ui.chk_fx_range.clicked.connect(self.draw_impz_time) self.ui.chk_win_time.clicked.connect(self.draw_impz_time) # --- frequency domain plotting --- self.ui.cmb_plt_freq_resp.currentIndexChanged.connect( self.draw_impz_freq) self.ui.chk_mrk_freq_resp.clicked.connect(self.draw_impz_freq) self.ui.cmb_plt_freq_stim.currentIndexChanged.connect( self.draw_impz_freq) self.ui.chk_mrk_freq_stim.clicked.connect(self.draw_impz_freq) self.ui.chk_log_freq.clicked.connect(self._log_mode_freq) self.ui.led_log_bottom_freq.editingFinished.connect( self._log_mode_freq) self.ui.chk_win_freq.clicked.connect(self.draw_impz_freq) # frequency widgets require special handling as they are scaled with f_s self.ui.ledFreq1.installEventFilter(self) self.ui.ledFreq2.installEventFilter(self) self.mplwidget_t.mplToolbar.sig_tx.connect( self.process_sig_rx) # connect to toolbar self.mplwidget_f.mplToolbar.sig_tx.connect( self.process_sig_rx) # connect to toolbar # When user has selected a different tab, trigger a draw (incl. maybe recalc) of current tab self.tabWidget.currentChanged.connect( self.draw) # passes number of active tab self.sig_rx.connect(self.ui.sig_rx) self.ui.sig_tx.connect( self.process_sig_rx) # connect to widgets and signals upstream #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and input_tab_widgets """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_draw, self.isVisible())) if dict_sig['sender'] == __name__: logger.warning("Stopped infinite loop, {0}".format(dict_sig)) if 'fx_sim' in dict_sig: try: if dict_sig['fx_sim'] == 'set_hdl_dict': self.fx_set_hdl_dict(dict_sig) # pass hdl dict if dict_sig['fx_sim'] == 'get_stimulus': # read hdl_dict and calculate stimulus self.hdl_dict = dict_sig['hdl_dict'] self.calc_stimulus( ) # calculate selected stimulus with selected length # pass stimulus in self.x back via dict self.sig_tx.emit({ 'sender': __name__, 'fx_sim': 'set_stimulus', 'fx_stimulus': self.x }) elif dict_sig['fx_sim'] == 'set_results': logger.info("Received fixpoint results.") self.fx_set_results(dict_sig) # plot fx simulation results except KeyError as e: logger.error('Key {0} missing in "hdl_dict".'.format(e)) self.fx_sim = False if self.isVisible(): if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\ or 'view_changed' in dict_sig or self.needs_draw: # todo: after 'data_changed' all needs to be set to True except current widget self.draw() elif 'home' in dict_sig: self.redraw() # self.tabWidget.currentWidget().redraw() # redraw method of current mplwidget, always redraws tab 0 self.needs_redraw[self.tabWidget.currentIndex()] = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.needs_redraw[self.tabWidget.currentIndex()]: self.needs_redraw[:] = [True] * 3 self.redraw() # redraw current widget else: if 'data_changed' in dict_sig or 'specs_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig[ 'ui_changed'] == 'resized': self.needs_redraw[:] = [True] * 3 #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the monitored widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (``QEvent.FocusIn``), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (``QEvent.FocusOut``), store current value normalized to f_S with full precision (only if ``spec_edited == True``) and display the stored value in selected format """ def _store_entry(source): if self.spec_edited: if source.objectName() == "stimFreq1": self.f1 = safe_eval(source.text(), self.f1 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText( str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": self.f2 = safe_eval(source.text(), self.f2 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText( str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) self.spec_edited = False # reset flag self.draw() # if isinstance(source, QLineEdit): # if source.objectName() in {"stimFreq1","stimFreq2"}: if event.type() in {QEvent.FocusIn, QEvent.KeyPress, QEvent.FocusOut}: if event.type() == QEvent.FocusIn: self.spec_edited = False self.load_fs() elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {Qt.Key_Return, Qt.Key_Enter}: _store_entry(source) elif key == Qt.Key_Escape: # revert changes self.spec_edited = False if source.objectName() == "stimFreq1": source.setText( str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": source.setText( str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) elif event.type() == QEvent.FocusOut: _store_entry(source) # Call base class method to continue normal event processing: return super(Plot_Impz, self).eventFilter(source, event) #------------------------------------------------------------- def load_fs(self): """ Reload sampling frequency from filter dictionary and transform the displayed frequency spec input fields according to the units setting (i.e. f_S). Spec entries are always stored normalized w.r.t. f_S in the dictionary; when f_S or the unit are changed, only the displayed values of the frequency entries are updated, not the dictionary! load_fs() is called during init and when the frequency unit or the sampling frequency have been changed. It should be called when sigSpecsChanged or sigFilterDesigned is emitted at another place, indicating that a reload is required. """ # recalculate displayed freq spec values for (maybe) changed f_S if self.ui.ledFreq1.hasFocus(): # widget has focus, show full precision self.ui.ledFreq1.setText(str(self.f1 * fb.fil[0]['f_S'])) elif self.ui.ledFreq2.hasFocus(): # widget has focus, show full precision self.ui.ledFreq2.setText(str(self.f2 * fb.fil[0]['f_S'])) else: # widgets have no focus, round the display self.ui.ledFreq1.setText( str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) self.ui.ledFreq2.setText( str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) #------------------------------------------------------------- def _show_stim_options(self): """ Hide / show panel with stimulus options """ self.ui.wdg_ctrl_stim.setVisible(self.ui.chk_stim_options.isChecked()) # ============================================================================= def fx_select(self): """ Select between fixpoint and floating point simulation """ self.sim_select = qget_cmb_box(self.ui.cmb_sim_select, data=False) self.fx_sim = (self.sim_select == 'Fixpoint') self.ui.but_run.setVisible(self.fx_sim) self.ui.chk_fx_scale.setVisible(self.fx_sim) self.ui.chk_fx_range.setVisible(self.fx_sim) self.hdl_dict = None if self.fx_sim: qstyle_widget(self.ui.but_run, "changed") self.fx_run() else: self.draw() def fx_run(self): """ Run fixpoint simulation """ self.sig_tx.emit({'sender': __name__, 'fx_sim': 'start'}) def fx_set_hdl_dict(self, dict_sig): """ Set quantization dict """ try: self.hdl_dict = dict_sig['hdl_dict'] except (KeyError, ValueError) as e: logger.warning(e) def fx_set_results(self, dict_sig): """ Get simulation results from `dict_sig` and transfer them to plotting routine. """ self.calc_response(dict_sig['fx_results']) qset_cmb_box(self.ui.cmb_sim_select, "Fixpoint", fireSignals=True) self.calc_fft() self.draw_impz() #------------------------------------------------------------------------------ def calc_stimulus(self): """ (Re-)calculate stimulus `self.x` """ self.n = np.arange(self.ui.N_end) self.t = self.n / fb.fil[0]['f_S'] # calculate stimuli x[n] ============================================== if self.ui.stim == "Pulse": self.x = np.zeros(self.ui.N_end) self.x[0] = self.ui.A1 # create dirac impulse as input signal self.title_str = r'Impulse Response' self.H_str = r'$h[n]$' # default elif self.ui.stim == "None": self.x = np.zeros(self.ui.N_end) self.title_str = r'System Response to Zero Input' self.H_str = r'$h_0[n]$' # default elif self.ui.stim == "Step": self.x = self.ui.A1 * np.ones( self.ui.N_end) # create step function self.title_str = r'Step Response' self.H_str = r'$h_{\epsilon}[n]$' elif self.ui.stim == "StepErr": self.x = self.ui.A1 * np.ones( self.ui.N_end) # create step function self.title_str = r'Settling Error' self.H_str = r'$h_{\epsilon, \infty} - h_{\epsilon}[n]$' elif self.ui.stim == "Cos": self.x = self.ui.A1 * np.cos(2 * np.pi * self.n * self.f1) +\ self.ui.A2 * np.cos(2 * np.pi * self.n * self.f2 + self.ui.phi2) self.title_str = r'System Response to Cosine Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Sine": self.x = self.ui.A1 * np.sin(2 * np.pi * self.n * self.f1 + self.ui.phi1) +\ self.ui.A2 * np.sin(2 * np.pi * self.n * self.f2 + self.ui.phi2) self.title_str = r'System Response to Sinusoidal Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Rect": self.x = self.ui.A1 * np.sign(np.sin(2 * np.pi * self.n * self.f1)) self.title_str = r'System Response to Rect. Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Saw": self.x = self.ui.A1 * sig.sawtooth(self.n * self.f1 * 2 * np.pi) self.title_str = r'System Response to Sawtooth Signal' self.H_str = r'$y[n]$' else: logger.error('Unknown stimulus "{0}"'.format(self.ui.stim)) return # Add noise to stimulus if self.ui.noise == "gauss": self.x[self.ui.N_start:] += self.ui.noi * np.random.randn( self.ui.N) self.title_str += r' w/ Gaussian Noise' elif self.ui.noise == "uniform": self.x[self.ui.N_start:] += self.ui.noi * ( np.random.rand(self.ui.N) - 0.5) self.title_str += r' w/ Uniform Noise' # Add DC to stimulus when visible / enabled if self.ui.ledDC.isVisible: self.x += self.ui.DC if self.ui.DC != 0: self.title_str += r' and DC' if self.fx_sim: self.title_str = r'$Fixpoint$ ' + self.title_str self.needs_redraw[:] = [True] * 2 #------------------------------------------------------------------------------ def calc_response(self, y_fx=None): """ (Re-)calculate filter response `self.y` from either stimulus `self.x` (float mode) or copy fixpoint response. Split response into imag. and real components `self.y_i` and `self.y_r` and set the flag `self.cmplx`. """ if self.fx_sim: # use fixpoint simulation results instead of floating results if y_fx is not None: self.y = np.array(y_fx) qstyle_widget(self.ui.but_run, "normal") else: self.y = None else: # calculate response self.y_r[n] and self.y_i[n] (for complex case) ===== self.bb = np.asarray(fb.fil[0]['ba'][0]) self.aa = np.asarray(fb.fil[0]['ba'][1]) if min(len(self.aa), len(self.bb)) < 2: logger.error( 'No proper filter coefficients: len(a), len(b) < 2 !') return logger.info("Coefficient area = {0}".format(np.sum(np.abs( self.bb)))) sos = np.asarray(fb.fil[0]['sos']) antiCausal = 'zpkA' in fb.fil[0] causal = not (antiCausal) if len(sos ) > 0 and causal: # has second order sections and is causal y = sig.sosfilt(sos, self.x) elif antiCausal: y = sig.filtfilt(self.bb, self.aa, self.x, -1, None) else: # no second order sections or antiCausals for current filter y = sig.lfilter(self.bb, self.aa, self.x) if self.ui.stim == "StepErr": dc = sig.freqz(self.bb, self.aa, [0]) # DC response of the system y = y - abs(dc[1]) # subtract DC (final) value from response self.y = np.real_if_close( y, tol=1e3) # tol specified in multiples of machine eps self.needs_redraw[0] = True self.needs_redraw[1] = True # Calculate imag. and real components from response self.cmplx = np.any(np.iscomplex(self.y)) if self.cmplx: self.y_i = self.y.imag self.y_r = self.y.real else: self.y_r = self.y self.y_i = None #------------------------------------------------------------------------------ def calc_fft(self): """ (Re-)calculate ffts X(f) and Y(f) of stimulus and response """ #if self.plt_freq_resp != "none": # calculate FFT of stimulus / response if self.x is None or len(self.x) < self.ui.N_end: self.X = np.zeros(self.ui.N_end - self.ui.N_start) # dummy result if self.x is None: logger.warning("Stimulus is 'None', FFT cannot be calculated.") else: logger.warning( "Length of stimulus is {0} < N = {1}, FFT cannot be calculated." .format(len(self.x), self.ui.N_end)) else: x_win = self.x[self.ui.N_start:self.ui.N_end] * self.ui.win self.X = np.abs(np.fft.fft(x_win)) / self.ui.N #if self.plt_freq_resp != "none": if self.y is None or len(self.y) < self.ui.N_end: self.Y = np.zeros(self.ui.N_end - self.ui.N_start) # dummy result if self.y is None: logger.warning( "Transient response is 'None', FFT cannot be calculated.") else: logger.warning( "Length of transient response is {0} < N = {1}, FFT cannot be calculated." .format(len(self.y), self.ui.N_end)) else: y_win = self.y[self.ui.N_start:self.ui.N_end] * self.ui.win self.Y = np.abs(np.fft.fft(y_win)) / self.ui.N if self.ui.chk_win_freq.isChecked(): self.Win = np.abs(np.fft.fft(self.ui.win)) / self.ui.N self.needs_redraw[1] = True #------------------------------------------------------------------------------ def update_view(self): """ Only update the limits without recalculating the impulse response """ self.draw_impz() ############################################################################### # PLOTTING ############################################################################### def draw(self): """ Recalculate response and redraw it """ if True: # self.needs_draw: doesn't work yet - number of data points needs to updated self.calc_stimulus() self.calc_response() self.needs_draw = False self.draw_impz() #------------------------------------------------------------------------------ def draw_impz(self): """ (Re-)draw the figure without recalculation """ if not hasattr( self, 'cmplx' ): # has response been calculated yet? logger.error("self.y {0}".format(self.y)) self.calc_stimulus() self.calc_response() f_unit = fb.fil[0]['freq_specs_unit'] if f_unit in {"f_S", "f_Ny"}: unit_frmt = "i" # italic else: unit_frmt = None self.ui.lblFreqUnit1.setText(to_html(f_unit, frmt=unit_frmt)) self.ui.lblFreqUnit2.setText(to_html(f_unit, frmt=unit_frmt)) self.load_fs() #self.init_axes() self.fmt_plot_resp = {'color': 'red', 'linewidth': 2, 'alpha': 0.5} self.fmt_mkr_resp = {'color': 'red', 'alpha': 0.5} self.fmt_plot_stim = {'color': 'blue', 'linewidth': 2, 'alpha': 0.5} self.fmt_mkr_stim = {'color': 'blue', 'alpha': 0.5} self.fmt_stem_stim = params['mpl_stimuli'] idx = self.tabWidget.currentIndex() if idx == 0: self.draw_impz_time() elif idx == 1: self.draw_impz_freq() else: logger.error("Index {0} out of range!".format(idx)) #================ Plotting routine time domain ========================= def _log_mode_time(self): """ Select / deselect log. mode for time domain and update self.bottom_t """ log = self.ui.chk_log_time.isChecked() self.ui.lbl_log_bottom_time.setVisible(log) self.ui.led_log_bottom_time.setVisible(log) self.ui.lbl_dB_time.setVisible(log) if log: self.bottom_t = safe_eval(self.ui.led_log_bottom_time.text(), self.bottom_t, return_type='float', sign='neg') self.ui.led_log_bottom_time.setText(str(self.bottom_t)) else: self.bottom_t = 0 self.draw_impz() def _log_mode_freq(self): """ Select / deselect log. mode for frequency domain and update self.bottom_f """ log = self.ui.chk_log_freq.isChecked() self.ui.lbl_log_bottom_freq.setVisible(log) self.ui.led_log_bottom_freq.setVisible(log) self.ui.lbl_dB_freq.setVisible(log) if log: self.bottom_f = safe_eval(self.ui.led_log_bottom_freq.text(), self.bottom_f, return_type='float', sign='neg') self.ui.led_log_bottom_freq.setText(str(self.bottom_f)) else: self.bottom_f = 0 self.draw_impz() def plot_fnc(self, plt_style, ax, plt_dict=None, bottom=0): """ Return a plot method depending on the parameter `plt_style` (str) and the axis instance `ax`. An optional `plt_dict` is modified in place. """ if plt_dict is None: plt_dict = {} if plt_style == "line": plot_fnc = getattr(ax, "plot") elif plt_style == "stem": plot_fnc = stems plt_dict.update({'ax': ax, 'bottom': bottom}) elif plt_style == "step": plot_fnc = getattr(ax, "plot") plt_dict.update({'drawstyle': 'steps-mid'}) elif plt_style == "dots": plot_fnc = getattr(ax, "scatter") elif plt_style == "none": plot_fnc = no_plot else: plot_fnc = no_plot return plot_fnc def _init_axes_time(self): """ Clear the axes of the time domain matplotlib widgets and (re)draw the plots. """ self.plt_time_stim = qget_cmb_box(self.ui.cmb_plt_time_stim, data=False).lower() self.plt_time_resp = qget_cmb_box(self.ui.cmb_plt_time_resp, data=False).lower() plt_time = self.plt_time_resp != "none" or self.plt_time_stim != "none" self.mplwidget_t.fig.clf() # clear figure with axes if plt_time: num_subplots = 1 + (self.cmplx and self.plt_time_resp != "none") self.mplwidget_t.fig.subplots_adjust(hspace=0.5) self.ax_r = self.mplwidget_t.fig.add_subplot(num_subplots, 1, 1) self.ax_r.clear() self.ax_r.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_r.get_yaxis().tick_left() # remove axis ticks right if self.cmplx and self.plt_time_resp != "none": self.ax_i = self.mplwidget_t.fig.add_subplot(num_subplots, 1, 2, sharex=self.ax_r) self.ax_i.clear() self.ax_i.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_i.get_yaxis().tick_left() # remove axis ticks right if self.ACTIVE_3D: # not implemented / tested yet self.ax3d = self.mplwidget_t.fig.add_subplot(111, projection='3d') def draw_impz_time(self): """ (Re-)draw the time domain mplwidget """ if self.y is None: # safety net for empty responses for ax in self.mplwidget_t.fig.get_axes(): # remove all axes self.mplwidget_t.fig.delaxes(ax) return mkfmt_i = 'd' self._init_axes_time() scale_i = scale_o = 1 fx_min = -1. fx_max = 1. fx_title = "" if self.fx_sim: # fixpoint simulation enabled -> scale stimulus and response WI = WO = 1 try: logger.info("hdl_dict = {0}".format(self.hdl_dict)) WI = self.hdl_dict['QI']['W'] WO = self.hdl_dict['QO']['W'] except AttributeError as e: logger.error("Attribute error: {0}".format(e)) except TypeError as e: logger.error("Type error: 'hdl_dict'={0},\n{1}".format( self.hdl_dict, e)) except ValueError as e: logger.error("Value error: {0}".format(e)) if self.ui.chk_fx_scale.isChecked(): scale_i = 1 << WI - 1 fx_min = -(1 << WO - 1) fx_max = (1 << WO - 1) - 1 else: scale_o = 1. / (1 << WO - 1) fx_min = -1 fx_max = 1 - scale_o logger.info("scale I:{0} O:{1}".format(scale_i, scale_o)) if self.ui.chk_log_time.isChecked( ): # log. scale for stimulus / response time domain H_str = '$|$' + self.H_str + '$|$ in dBV' x = np.maximum(20 * np.log10(abs(self.x * scale_i)), self.bottom_t) y = np.maximum(20 * np.log10(abs(self.y_r * scale_o)), self.bottom_t) win = np.maximum(20 * np.log10(abs(self.ui.win)), self.bottom_t) if self.cmplx: y_i = np.maximum(20 * np.log10(abs(self.y_i)), self.bottom_t) H_i_str = r'$|\Im\{$' + self.H_str + '$\}|$' + ' in dBV' H_str = r'$|\Re\{$' + self.H_str + '$\}|$' + ' in dBV' fx_min = 20 * np.log10(abs(fx_min)) fx_max = fx_min else: x = self.x * scale_i y = self.y_r * scale_o win = self.ui.win if self.cmplx: y_i = self.y_i * scale_o if self.cmplx: H_i_str = r'$\Im\{$' + self.H_str + '$\}$ in V' H_str = r'$\Re\{$' + self.H_str + '$\}$ in V' else: H_str = self.H_str + ' in V' if self.ui.chk_fx_range.isChecked() and self.fx_sim: self.ax_r.axhline(fx_max, 0, 1, color='k', linestyle='--') self.ax_r.axhline(fx_min, 0, 1, color='k', linestyle='--') # --------------- Stimulus plot ---------------------------------- plot_stim_dict = self.fmt_plot_stim.copy() plot_stim_fnc = self.plot_fnc(self.plt_time_stim, self.ax_r, plot_stim_dict, self.bottom_t) plot_stim_fnc(self.t[self.ui.N_start:], x[self.ui.N_start:], label='$x[n]$', **plot_stim_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_stim.isChecked( ) and self.plt_time_stim not in {"dots", "none"}: self.ax_r.scatter(self.t[self.ui.N_start:], x[self.ui.N_start:], **self.fmt_mkr_stim) # --------------- Response plot ---------------------------------- plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_time_resp, self.ax_r, plot_resp_dict, self.bottom_t) plot_resp_fnc(self.t[self.ui.N_start:], y[self.ui.N_start:], label='$y[n]$', **plot_resp_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_resp.isChecked( ) and self.plt_time_resp not in {"dots", "none"}: self.ax_r.scatter(self.t[self.ui.N_start:], y[self.ui.N_start:], **self.fmt_mkr_resp) # --------------- Window plot ---------------------------------- if self.ui.chk_win_time.isChecked(): self.ax_r.plot(self.t[self.ui.N_start:], win, c="gray", label=self.ui.window_type) self.ax_r.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.5) # --------------- Complex response ---------------------------------- if self.cmplx and self.plt_time_resp != "none": #plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_time_resp, self.ax_i, plot_resp_dict, self.bottom_t) plot_resp_fnc(self.t[self.ui.N_start:], y_i[self.ui.N_start:], label='$y_i[n]$', **plot_resp_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_resp.isChecked( ) and self.plt_time_resp not in {"dots", "none"}: self.ax_i.scatter(self.t[self.ui.N_start:], y_i[self.ui.N_start:], marker=mkfmt_i, **self.fmt_mkr_resp) # [ml_i, sl_i, bl_i] = self.ax_i.stem(self.t[self.ui.N_start:], y_i[self.ui.N_start:], # bottom=self.bottom_t, markerfmt=mkfmt_i, label = '$y_i[n]$') # self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel']) # self.ax_r.get_xaxis().set_ticklabels([]) # removes both xticklabels # plt.setp(ax_r.get_xticklabels(), visible=False) # is shorter but imports matplotlib, set property directly instead: [label.set_visible(False) for label in self.ax_r.get_xticklabels()] self.ax_r.set_ylabel(H_str + r'$\rightarrow $') self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_i.set_ylabel(H_i_str + r'$\rightarrow $') self.ax_i.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.5) else: self.ax_r.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_r.set_ylabel(H_str + r'$\rightarrow $') self.ax_r.set_title(fx_title + self.title_str) self.ax_r.set_xlim( [self.t[self.ui.N_start], self.t[self.ui.N_end - 1]]) expand_lim(self.ax_r, 0.02) if self.ACTIVE_3D: # not implemented / tested yet # plotting the stems for i in range(self.ui.N_start, self.ui.N_end): self.ax3d.plot([self.t[i], self.t[i]], [y[i], y[i]], [0, y_i[i]], '-', linewidth=2, alpha=.5) # plotting a circle on the top of each stem self.ax3d.plot(self.t[self.ui.N_start:], y[self.ui.N_start:], y_i[self.ui.N_start:], 'o', markersize=8, markerfacecolor='none', label='$y[n]$') self.ax3d.set_xlabel('x') self.ax3d.set_ylabel('y') self.ax3d.set_zlabel('z') self.redraw() # redraw currently active mplwidget #-------------------------------------------------------------------------- def _init_axes_freq(self): """ Clear the axes of the frequency domain matplotlib widgets and calculate the fft """ self.plt_freq_stim = qget_cmb_box(self.ui.cmb_plt_freq_stim, data=False).lower() self.plt_freq_resp = qget_cmb_box(self.ui.cmb_plt_freq_resp, data=False).lower() self.plt_freq_disabled = self.plt_freq_stim == "none" and self.plt_freq_resp == "none" if not self.ui.chk_log_freq.isChecked() and len( self.mplwidget_f.fig.get_axes()) == 2: self.mplwidget_f.fig.clear( ) # get rid of second axis when returning from log mode by clearing all if len(self.mplwidget_f.fig.get_axes()) == 0: # empty figure, no axes self.ax_fft = self.mplwidget_f.fig.add_subplot(111) self.ax_fft.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_fft.get_yaxis().tick_left() # remove axis ticks right self.ax_fft.set_title("FFT of Transient Response") for ax in self.mplwidget_f.fig.get_axes( ): # clear but don't delete all axes ax.cla() if self.ui.chk_log_freq.isChecked() and len( self.mplwidget_f.fig.get_axes()) == 1: # create second axis scaled for noise power scale if it doesn't exist yet self.ax_fft_noise = self.ax_fft.twinx() self.ax_fft_noise.is_twin = True self.calc_fft() def draw_impz_freq(self): """ (Re-)draw the frequency domain mplwidget """ self._init_axes_freq() plt_response = self.plt_freq_resp != "none" plt_stimulus = self.plt_freq_stim != "none" #if self.plt_freq != "None": if not self.plt_freq_disabled: if plt_response and not plt_stimulus: XY_str = r'$|Y(\mathrm{e}^{\mathrm{j} \Omega})|$' elif not plt_response and plt_stimulus: XY_str = r'$|X(\mathrm{e}^{\mathrm{j} \Omega})|$' else: XY_str = r'$|X,Y(\mathrm{e}^{\mathrm{j} \Omega})|$' F = np.fft.fftfreq(self.ui.N, d=1. / fb.fil[0]['f_S']) if plt_stimulus: X = self.X.copy() / np.sqrt( 2) # enforce deep copy and convert to RMS self.Px = np.sum(np.square(self.X)) if fb.fil[0]['freqSpecsRangeType'] == 'half': X[1:] = 2 * X[ 1:] # correct for single-sided spectrum (except DC) if plt_response: Y = self.Y.copy() / np.sqrt( 2) # enforce deep copy and convert to RMS self.Py = np.sum(np.square(self.Y)) if fb.fil[0]['freqSpecsRangeType'] == 'half': Y[1:] = 2 * Y[ 1:] # correct for single-sided spectrum (except DC) if self.ui.chk_win_freq.isChecked(): Win = self.Win.copy() / np.sqrt( 2) # enforce deep copy and convert to RMS if fb.fil[0]['freqSpecsRangeType'] == 'half': Win[1:] = 2 * Win[ 1:] # correct for single-sided spectrum (except DC) if self.ui.chk_log_freq.isChecked(): unit = unit_P = "dBW" unit_nenbw = "dB" nenbw = 10 * np.log10(self.ui.nenbw) if plt_stimulus: X = np.maximum(20 * np.log10(X), self.ui.bottom_f) self.Px = 10 * np.log10(self.Px) if plt_response: Y = np.maximum(20 * np.log10(Y), self.ui.bottom_f) self.Py = 10 * np.log10(self.Py) if self.ui.chk_win_freq.isChecked(): Win = np.maximum(20 * np.log10(Win), self.ui.bottom_f) else: unit = "Vrms" unit_P = "W" unit_nenbw = "bins" nenbw = self.ui.nenbw XY_str = XY_str + ' in ' + unit if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift X, Y and F by f_S/2 if plt_response: Y = np.fft.fftshift(Y) if plt_stimulus: X = np.fft.fftshift(X) if self.ui.chk_win_freq.isChecked(): Win = np.fft.fftshift(Win) F = np.fft.fftshift(F) elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of X, Y and F if plt_response: Y = Y[0:self.ui.N // 2] if plt_stimulus: X = X[0:self.ui.N // 2] if self.ui.chk_win_freq.isChecked(): Win = Win[0:self.ui.N // 2] F = F[0:self.ui.N // 2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # plot for F = 0 ... 1 F = np.fft.fftshift(F) + fb.fil[0]['f_S'] / 2. # --------------- Plot stimulus and response ---------------------- labels = [] if plt_stimulus: plot_stim_dict = self.fmt_plot_stim.copy() plot_stim_fnc = self.plot_fnc(self.plt_freq_stim, self.ax_fft, plot_stim_dict, self.bottom_f) plot_stim_fnc(F, X, label='$Stim.$', **plot_stim_dict) if self.ui.chk_mrk_freq_stim.isChecked( ) and self.plt_freq_stim not in {"dots", "none"}: self.ax_fft.scatter(F, X, **self.fmt_mkr_stim) labels.append("$P_X$ = {0:.3g} {1}".format(self.Px, unit_P)) if plt_response: plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_freq_resp, self.ax_fft, plot_resp_dict, self.bottom_f) plot_resp_fnc(F, Y, label='$Resp.$', **plot_resp_dict) if self.ui.chk_mrk_freq_resp.isChecked( ) and self.plt_freq_resp not in {"dots", "none"}: self.ax_fft.scatter(F, Y, **self.fmt_mkr_resp) labels.append("$P_Y$ = {0:.3g} {1}".format(self.Py, unit_P)) if self.ui.chk_win_freq.isChecked(): self.ax_fft.plot(F, Win, c="gray", label="win") labels.append("{0}".format(self.ui.window_type)) labels.append("$NENBW$ = {0:.4g} {1}".format(nenbw, unit_nenbw)) labels.append("$CGAIN$ = {0:.4g}".format(self.ui.scale)) # collect all plot objects, hope that the order isn't messed up and add two dummy handles # for the NENBW and the CGAIN labels handles = self.ax_fft.get_lines() handles.append( mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0)) handles.append( mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0)) self.ax_fft.legend(handles, labels, loc='best', fontsize='small', fancybox=True, framealpha=0.5) self.ax_fft.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_fft.set_ylabel(XY_str) self.ax_fft.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_fft.set_title(self.title_str) if self.ui.chk_log_freq.isChecked(): # scale second axis for noise power corr = 10 * np.log10(self.ui.N / self.ui.nenbw) mn, mx = self.ax_fft.get_ylim() self.ax_fft_noise.set_ylim(mn + corr, mx + corr) self.ax_fft_noise.set_ylabel(r'$P_N$ in dBW') self.redraw() # redraw currently active mplwidget #------------------------------------------------------------------------------ def redraw(self): """ Redraw the currently visible canvas when e.g. the canvas size has changed """ idx = self.tabWidget.currentIndex() self.tabWidget.currentWidget().redraw() #wdg = getattr(self, self.tab_mplwidgets[idx]) logger.debug("Redrawing tab {0}".format(idx)) #wdg_cur.redraw() self.needs_redraw[idx] = False
class Plot_PZ(QWidget): # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self, parent): super(Plot_PZ, self).__init__(parent) self.needs_calc = True # flag whether filter data has been changed self.needs_draw = False # flag whether whether figure needs to be drawn # with new limits etc. (not implemented yet) self.tool_tip = "Pole / zero plan" self.tab_label = "P / Z" self._construct_UI() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_calc, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False self.needs_draw = False if 'view_changed' in dict_sig or self.needs_draw: self.update_view() self.needs_draw = False else: if 'data_changed' in dict_sig: self.needs_calc = True if 'view_changed' in dict_sig: self.needs_draw = True #------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkHf = QCheckBox("Show |H(f)|", self) self.chkHf.setToolTip( "<span>Display |H(f)| around unit circle.</span>") self.chkHf.setEnabled(True) self.chkHfLog = QCheckBox("Log. Scale", self) self.chkHfLog.setToolTip("<span>Log. scale for |H(f)|.</span>") self.chkHfLog.setEnabled(True) self.diaRad_Hf = QDial(self) self.diaRad_Hf.setRange(2., 10.) self.diaRad_Hf.setValue(2) self.diaRad_Hf.setTracking(False) # produce less events when turning self.diaRad_Hf.setFixedHeight(30) self.diaRad_Hf.setFixedWidth(30) self.diaRad_Hf.setWrapping(False) self.diaRad_Hf.setToolTip( "<span>Set max. radius for |H(f)| plot.</span>") self.lblRad_Hf = QLabel("Radius", self) self.chkFIR_P = QCheckBox("Plot FIR Poles", self) self.chkFIR_P.setToolTip("<span>Show FIR poles at the origin.</span>") self.chkFIR_P.setChecked(True) layHControls = QHBoxLayout() layHControls.addWidget(self.chkHf) layHControls.addWidget(self.chkHfLog) layHControls.addWidget(self.diaRad_Hf) layHControls.addWidget(self.lblRad_Hf) layHControls.addStretch(10) layHControls.addWidget(self.chkFIR_P) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw poles and zeros #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.chkHf.clicked.connect(self.draw) self.chkHfLog.clicked.connect(self.draw) self.diaRad_Hf.valueChanged.connect(self.draw) self.chkFIR_P.clicked.connect(self.draw) #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes (this is only run once) """ if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes self.ax = self.mplwidget.fig.subplots() #.add_subplot(111) self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etcs without recalculating H(f) -- not yet implemented, just use draw() for the moment """ self.draw() #------------------------------------------------------------------------------ def draw(self): self.chkFIR_P.setVisible(fb.fil[0]['ft'] == 'FIR') self.draw_pz() #------------------------------------------------------------------------------ def draw_pz(self): """ (re)draw P/Z plot """ p_marker = params['P_Marker'] z_marker = params['Z_Marker'] zpk = fb.fil[0]['zpk'] # add antiCausals if they exist (must take reciprocal to plot) if 'rpk' in fb.fil[0]: zA = fb.fil[0]['zpk'][0] zA = np.conj(1. / zA) pA = fb.fil[0]['zpk'][1] pA = np.conj(1. / pA) zC = np.append(zpk[0], zA) pC = np.append(zpk[1], pA) zpk[0] = zC zpk[1] = pC self.ax.clear() [z, p, k] = self.zplane(z=zpk[0], p=zpk[1], k=zpk[2], plt_ax=self.ax, plt_poles=self.chkFIR_P.isChecked() or fb.fil[0]['ft'] == 'IIR', mps=p_marker[0], mpc=p_marker[1], mzs=z_marker[0], mzc=z_marker[1]) self.ax.set_title(r'Pole / Zero Plot') self.ax.set_xlabel('Real axis') self.ax.set_ylabel('Imaginary axis') self.draw_Hf(r=self.diaRad_Hf.value()) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw() #------------------------------------------------------------------------------ def zplane(self, b=None, a=1, z=None, p=None, k=1, pn_eps=1e-3, analog=False, plt_ax=None, plt_poles=True, style='square', anaCircleRad=0, lw=2, mps=10, mzs=10, mpc='r', mzc='b', plabel='', zlabel=''): """ Plot the poles and zeros in the complex z-plane either from the coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False) or directly from the zeros and poles (z,p) (zpk = True). When only b is given, an FIR filter with all poles at the origin is assumed. Parameters ---------- b : array_like Numerator coefficients (transversal part of filter) When b is not None, poles and zeros are determined from the coefficients b and a a : array_like (optional, default = 1 for FIR-filter) Denominator coefficients (recursive part of filter) z : array_like, default = None Zeros When b is None, poles and zeros are taken directly from z and p p : array_like, default = None Poles analog : boolean (default: False) When True, create a P/Z plot suitable for the s-plane, i.e. suppress the unit circle (unless anaCircleRad > 0) and scale the plot for a good display of all poles and zeros. pn_eps : float (default : 1e-2) Tolerance for separating close poles or zeros plt_ax : handle to axes for plotting (default: None) When no axes is specified, the current axes is determined via plt.gca() plt_poles : Boolean (default : True) Plot poles. This can be used to suppress poles for FIR systems where all poles are at the origin. style : string (default: 'square') Style of the plot, for style == 'square' make scale of x- and y- axis equal. mps : integer (default: 10) Size for pole marker mzs : integer (default: 10) Size for zero marker mpc : char (default: 'r') Pole marker colour mzc : char (default: 'b') Zero marker colour lw : integer (default: 2) Linewidth for unit circle plabel, zlabel : string (default: '') This string is passed to the plot command for poles and zeros and can be displayed by legend() Returns ------- z, p, k : ndarray Notes ----- """ # TODO: # - polar option # - add keywords for color of circle -> **kwargs # - add option for multi-dimensional arrays and zpk data # make sure that all inputs are arrays b = np.atleast_1d(b) a = np.atleast_1d(a) z = np.atleast_1d(z) # make sure that p, z are arrays p = np.atleast_1d(p) if b.any(): # coefficients were specified if len(b) < 2 and len(a) < 2: logger.error( 'No proper filter coefficients: both b and a are scalars!') return z, p, k # The coefficients are less than 1, normalize the coefficients if np.max(b) > 1: kn = np.max(b) b = b / float(kn) else: kn = 1. if np.max(a) > 1: kd = np.max(a) a = a / abs(kd) else: kd = 1. # Calculate the poles, zeros and scaling factor p = np.roots(a) z = np.roots(b) k = kn / kd elif not (len(p) or len(z)): # P/Z were specified logger.error('Either b,a or z,p must be specified!') return z, p, k # find multiple poles and zeros and their multiplicities if len(p) < 2: # single pole, [None] or [0] if not p or p == 0: # only zeros, create equal number of poles at origin p = np.array(0, ndmin=1) # num_p = np.atleast_1d(len(z)) else: num_p = [1.] # single pole != 0 else: #p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg') p, num_p = unique_roots(p, tol=pn_eps, rtype='avg') # p = np.array(p); num_p = np.ones(len(p)) if len(z) > 0: z, num_z = unique_roots(z, tol=pn_eps, rtype='avg') # z = np.array(z); num_z = np.ones(len(z)) #z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg') else: num_z = [] ax = plt_ax #.subplot(111) if analog == False: # create the unit circle for the z-plane uc = patches.Circle((0, 0), radius=1, fill=False, color='grey', ls='solid', zorder=1) ax.add_patch(uc) if style == 'square': #r = 1.1 #ax.axis([-r, r, -r, r]) # overridden by next option ax.axis('equal') # ax.spines['left'].set_position('center') # ax.spines['bottom'].set_position('center') # ax.spines['right'].set_visible(True) # ax.spines['top'].set_visible(True) else: # s-plane if anaCircleRad > 0: # plot a circle with radius = anaCircleRad uc = patches.Circle((0, 0), radius=anaCircleRad, fill=False, color='grey', ls='solid', zorder=1) ax.add_patch(uc) # plot real and imaginary axis ax.axhline(lw=2, color='k', zorder=1) ax.axvline(lw=2, color='k', zorder=1) # Plot the zeros ax.scatter(z.real, z.imag, s=mzs * mzs, zorder=2, marker='o', facecolor='none', edgecolor=mzc, lw=lw, label=zlabel) # and print their multiplicity for i in range(len(z)): logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i])) if num_z[i] > 1: ax.text(np.real(z[i]), np.imag(z[i]), ' (' + str(num_z[i]) + ')', va='top', color=mzc) if plt_poles: # Plot the poles ax.scatter(p.real, p.imag, s=mps * mps, zorder=2, marker='x', color=mpc, lw=lw, label=plabel) # and print their multiplicity for i in range(len(p)): logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i])) if num_p[i] > 1: ax.text(np.real(p[i]), np.imag(p[i]), ' (' + str(num_p[i]) + ')', va='bottom', color=mpc) # ============================================================================= # # increase distance between ticks and labels # # to give some room for poles and zeros # for tick in ax.get_xaxis().get_major_ticks(): # tick.set_pad(12.) # tick.label1 = tick._get_text1() # for tick in ax.get_yaxis().get_major_ticks(): # tick.set_pad(12.) # tick.label1 = tick._get_text1() # # ============================================================================= xl = ax.get_xlim() Dx = max(abs(xl[1] - xl[0]), 0.05) yl = ax.get_ylim() Dy = max(abs(yl[1] - yl[0]), 0.05) ax.set_xlim((xl[0] - Dx * 0.05, max(xl[1] + Dx * 0.05, 0))) ax.set_ylim((yl[0] - Dy * 0.05, yl[1] + Dy * 0.05)) return z, p, k #------------------------------------------------------------------------------ def draw_Hf(self, r=2): """ Draw the magnitude frequency response around the UC """ # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') self.chkHfLog.setVisible(self.chkHf.isChecked()) self.diaRad_Hf.setVisible(self.chkHf.isChecked()) self.lblRad_Hf.setVisible(self.chkHf.isChecked()) if not self.chkHf.isChecked(): return ba = fb.fil[0]['ba'] w, H = sig.freqz(ba[0], ba[1], worN=params['N_FFT'], whole=True) H = np.abs(H) if self.chkHfLog.isChecked(): H = np.clip(np.log10(H), -6, None) # clip to -120 dB H = H - np.max(H) # shift scale to H_min ... 0 H = 1 + (r - 1) * (1 + H / abs(np.min(H))) # scale to 1 ... r else: H = 1 + (r - 1) * H / np.max(H) # map |H(f)| to a range 1 ... r y = H * np.sin(w) x = H * np.cos(w) self.ax.plot(x, y, label="|H(f)|") uc = patches.Circle((0, 0), radius=r, fill=False, color='grey', ls='dashed', zorder=1) self.ax.add_patch(uc) xl = self.ax.get_xlim() xmax = max(abs(xl[0]), abs(xl[1]), r * 1.05) yl = self.ax.get_ylim() ymax = max(abs(yl[0]), abs(yl[1]), r * 1.05) self.ax.set_xlim((-xmax, xmax)) self.ax.set_ylim((-ymax, ymax)) np.seterr(**old_settings_seterr)
def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # Define MplWidgets: Time domain plots #---------------------------------------------------------------------- self.mplwidget_t = MplWidget(self) self.mplwidget_t.setObjectName("mplwidget_t1") self.mplwidget_t.layVMainMpl.addWidget(self.ui.wdg_ctrl_time) self.mplwidget_t.layVMainMpl.setContentsMargins(*params['wdg_margins']) # MplWidget for frequency domain plots self.mplwidget_f = MplWidget(self) self.mplwidget_f.setObjectName("mplwidget_f1") self.mplwidget_f.layVMainMpl.addWidget(self.ui.wdg_ctrl_freq) self.mplwidget_f.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # Tabbed layout with vertical tabs #---------------------------------------------------------------------- self.tabWidget = QTabWidget(self) self.tabWidget.addTab(self.mplwidget_t, "Time") self.tabWidget.addTab(self.mplwidget_f, "Frequency") # list with tabWidgets self.tab_mplwidgets = ["mplwidget_t", "mplwidget_f"] self.tabWidget.setTabPosition(QTabWidget.West) layVMain = QVBoxLayout() layVMain.addWidget(self.tabWidget) layVMain.addWidget(self.ui.wdg_ctrl_stim) layVMain.addWidget(self.ui.wdg_ctrl_run) layVMain.setContentsMargins( *params['wdg_margins']) #(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # --- run control --- self.ui.cmb_sim_select.currentIndexChanged.connect(self.fx_select) self.ui.but_run.clicked.connect(self.fx_run) self.ui.chk_fx_scale.clicked.connect(self.draw_impz_time) self.ui.chk_stim_options.clicked.connect(self._show_stim_options) # --- time domain plotting --- self.ui.cmb_plt_time_resp.currentIndexChanged.connect( self.draw_impz_time) self.ui.chk_mrk_time_resp.clicked.connect(self.draw_impz_time) self.ui.cmb_plt_time_stim.currentIndexChanged.connect( self.draw_impz_time) self.ui.chk_mrk_time_stim.clicked.connect(self.draw_impz_time) self.ui.chk_log_time.clicked.connect(self._log_mode_time) self.ui.led_log_bottom_time.editingFinished.connect( self._log_mode_time) self.ui.chk_fx_range.clicked.connect(self.draw_impz_time) self.ui.chk_win_time.clicked.connect(self.draw_impz_time) # --- frequency domain plotting --- self.ui.cmb_plt_freq_resp.currentIndexChanged.connect( self.draw_impz_freq) self.ui.chk_mrk_freq_resp.clicked.connect(self.draw_impz_freq) self.ui.cmb_plt_freq_stim.currentIndexChanged.connect( self.draw_impz_freq) self.ui.chk_mrk_freq_stim.clicked.connect(self.draw_impz_freq) self.ui.chk_log_freq.clicked.connect(self._log_mode_freq) self.ui.led_log_bottom_freq.editingFinished.connect( self._log_mode_freq) self.ui.chk_win_freq.clicked.connect(self.draw_impz_freq) # frequency widgets require special handling as they are scaled with f_s self.ui.ledFreq1.installEventFilter(self) self.ui.ledFreq2.installEventFilter(self) self.mplwidget_t.mplToolbar.sig_tx.connect( self.process_sig_rx) # connect to toolbar self.mplwidget_f.mplToolbar.sig_tx.connect( self.process_sig_rx) # connect to toolbar # When user has selected a different tab, trigger a draw (incl. maybe recalc) of current tab self.tabWidget.currentChanged.connect( self.draw) # passes number of active tab self.sig_rx.connect(self.ui.sig_rx) self.ui.sig_tx.connect( self.process_sig_rx) # connect to widgets and signals upstream
class Plot_3D(QWidget): """ Class for various 3D-plots: - lin / log line plot of H(f) - lin / log surf plot of H(z) - optional display of poles / zeros """ # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self): super().__init__() self.zmin = 0 self.zmax = 4 self.zmin_dB = -80 self.cmap_default = 'RdYlBu' self.data_changed = True # flag whether data has changed self.tool_tip = "3D magnitude response |H(z)|" self.tab_label = "3D" self._construct_UI() # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from ``sig_rx`` """ # logger.debug("Processing {0} | data_changed = {1}, visible = {2}"\ # .format(dict_sig, self.data_changed, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.data_changed: self.draw() self.data_changed = False else: if 'data_changed' in dict_sig: self.data_changed = True # ------------------------------------------------------------------------------ def _construct_UI(self): self.but_log = PushButton("dB", checked=False) self.but_log.setObjectName("but_log") self.but_log.setToolTip("Logarithmic scale") self.but_plot_in_UC = PushButton("|z| < 1 ", checked=False) self.but_plot_in_UC.setObjectName("but_plot_in_UC") self.but_plot_in_UC.setToolTip("Only plot H(z) within the unit circle") self.lblBottom = QLabel(to_html("Bottom =", frmt='bi'), self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setToolTip("Minimum display value.") self.lblBottomdB = QLabel("dB", self) self.lblBottomdB.setVisible(self.but_log.isChecked()) self.lblTop = QLabel(to_html("Top =", frmt='bi'), self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.lblTopdB = QLabel("dB", self) self.lblTopdB.setVisible(self.but_log.isChecked()) self.plt_UC = PushButton("UC", checked=True) self.plt_UC.setObjectName("plt_UC") self.plt_UC.setToolTip("Plot unit circle") self.but_PZ = PushButton("P/Z ", checked=True) self.but_PZ.setObjectName("but_PZ") self.but_PZ.setToolTip("Plot poles and zeros") self.but_Hf = PushButton("H(f) ", checked=True) self.but_Hf.setObjectName("but_Hf") self.but_Hf.setToolTip("Plot H(f) along the unit circle") modes = ['None', 'Mesh', 'Surf', 'Contour'] self.cmbMode3D = QComboBox(self) self.cmbMode3D.addItems(modes) self.cmbMode3D.setObjectName("cmbShow3D") self.cmbMode3D.setToolTip("Select 3D-plot mode.") self.cmbMode3D.setCurrentIndex(0) self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.but_colormap_r = PushButton("reverse", checked=True) self.but_colormap_r.setObjectName("but_colormap_r") self.but_colormap_r.setToolTip("reverse colormap") self.cmbColormap = QComboBox(self) self._init_cmb_colormap(cmap_init=self.cmap_default) self.cmbColormap.setToolTip("Select colormap") self.but_colbar = PushButton("Colorbar ", checked=False) self.but_colbar.setObjectName("chkColBar") self.but_colbar.setToolTip("Show colorbar") self.but_lighting = PushButton("Lighting", checked=False) self.but_lighting.setObjectName("but_lighting") self.but_lighting.setToolTip("Enable light source") self.lblAlpha = QLabel(to_html("Alpha", frmt='bi'), self) self.diaAlpha = QDial(self) self.diaAlpha.setRange(0, 10) self.diaAlpha.setValue(10) self.diaAlpha.setTracking(False) # produce less events when turning self.diaAlpha.setFixedHeight(30) self.diaAlpha.setFixedWidth(30) self.diaAlpha.setWrapping(False) self.diaAlpha.setToolTip( "<span>Set transparency for surf and contour plots.</span>") self.lblHatch = QLabel(to_html("Stride", frmt='bi'), self) self.diaHatch = QDial(self) self.diaHatch.setRange(0, 9) self.diaHatch.setValue(5) self.diaHatch.setTracking(False) # produce less events when turning self.diaHatch.setFixedHeight(30) self.diaHatch.setFixedWidth(30) self.diaHatch.setWrapping(False) self.diaHatch.setToolTip("Set line density for various plots.") self.but_contour_2d = PushButton("Contour2D ", checked=False) self.but_contour_2d.setObjectName("chkContour2D") self.but_contour_2d.setToolTip("Plot 2D-contours at z =0") # ---------------------------------------------------------------------- # LAYOUT for UI widgets # ---------------------------------------------------------------------- layGControls = QGridLayout() layGControls.addWidget(self.but_log, 0, 0) layGControls.addWidget(self.but_plot_in_UC, 1, 0) layGControls.addWidget(self.lblTop, 0, 2) layGControls.addWidget(self.ledTop, 0, 4) layGControls.addWidget(self.lblTopdB, 0, 5) layGControls.addWidget(self.lblBottom, 1, 2) layGControls.addWidget(self.ledBottom, 1, 4) layGControls.addWidget(self.lblBottomdB, 1, 5) layGControls.setColumnStretch(5, 1) layGControls.addWidget(self.plt_UC, 0, 6) layGControls.addWidget(self.but_Hf, 1, 6) layGControls.addWidget(self.but_PZ, 0, 8) layGControls.addWidget(self.cmbMode3D, 0, 10) layGControls.addWidget(self.but_contour_2d, 1, 10) layGControls.addWidget(self.cmbColormap, 0, 12, 1, 1) layGControls.addWidget(self.but_colormap_r, 1, 12) layGControls.addWidget(self.but_lighting, 0, 14) layGControls.addWidget(self.but_colbar, 1, 14) layGControls.addWidget(self.lblAlpha, 0, 15) layGControls.addWidget(self.diaAlpha, 0, 16) layGControls.addWidget(self.lblHatch, 1, 15) layGControls.addWidget(self.diaHatch, 1, 16) # This widget encompasses all control subwidgets self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layGControls) # ---------------------------------------------------------------------- # mplwidget # ---------------------------------------------------------------------- # This is the plot pane widget, encompassing the other widgets self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_3d.html" self.setLayout(self.mplwidget.layVMainMpl) self._init_grid() # initialize grid and do initial plot # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_log.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.but_plot_in_UC.clicked.connect(self._init_grid) self.plt_UC.clicked.connect(self.draw) self.but_Hf.clicked.connect(self.draw) self.but_PZ.clicked.connect(self.draw) self.cmbMode3D.currentIndexChanged.connect(self.draw) self.but_colbar.clicked.connect(self.draw) self.cmbColormap.currentIndexChanged.connect(self.draw) self.but_colormap_r.clicked.connect(self.draw) self.but_lighting.clicked.connect(self.draw) self.diaAlpha.valueChanged.connect(self.draw) self.diaHatch.valueChanged.connect(self.draw) self.but_contour_2d.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) # self.mplwidget.mplToolbar.enable_plot(state = False) # disable initially # ------------------------------------------------------------------------------ def _init_cmb_colormap(self, cmap_init): """ Initialize combobox with available colormaps and try to set it to `cmap_init` Since matplotlib 3.2 the reversed "*_r" colormaps are no longer contained in `cm.datad`. They are now obtained by using the `reversed()` method (much simpler!) `cm.datad` doesn't return the "new" colormaps like viridis, instead the `colormaps()` method is used. """ self.cmbColormap.addItems( [m for m in colormaps() if not m.endswith("_r")]) idx = self.cmbColormap.findText(cmap_init) if idx == -1: idx = 0 self.cmbColormap.setCurrentIndex(idx) # ------------------------------------------------------------------------------ def _init_grid(self): """ Initialize (x,y,z) coordinate grid + (re)draw plot.""" phi_UC = np.linspace(0, 2 * pi, 400, endpoint=True) # angles for unit circle self.xy_UC = np.exp(1j * phi_UC) # x,y coordinates of unity circle steps = 100 # number of steps for x, y, r, phi # cartesian range limits self.xmin = -1.5 self.xmax = 1.5 self.ymin = -1.5 self.ymax = 1.5 # Polar range limits rmin = 0 rmax = 1 # Calculate grids for 3D-Plots dr = rmax / steps * 2 # grid size for polar range dx = (self.xmax - self.xmin) / steps dy = (self.ymax - self.ymin) / steps # grid size cartesian range if self.but_plot_in_UC.isChecked(): # Plot circular range in 3D-Plot [r, phi] = np.meshgrid(np.arange(rmin, rmax, dr), np.linspace(0, 2 * pi, steps, endpoint=True)) self.x = r * cos(phi) self.y = r * sin(phi) else: # cartesian grid [self.x, self.y] = np.meshgrid(np.arange(self.xmin, self.xmax, dx), np.arange(self.ymin, self.ymax, dy)) self.z = self.x + 1j * self.y # create coordinate grid for complex plane self.draw() # initial plot # ------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes to get rid of colorbar The azimuth / elevation / distance settings of the camera are restored after clearing the axes. See http://stackoverflow.com/questions/4575588/matplotlib-3d-plot-with-pyqt4-in-qtabwidget-mplwidget """ self._save_axes() self.mplwidget.fig.clf() # needed to get rid of colorbar self.ax3d = self.mplwidget.fig.add_subplot(111, projection='3d') # self.ax3d = self.mplwidget.fig.subplots(nrows=1, ncols=1, projection='3d') self._restore_axes() # ------------------------------------------------------------------------------ def _save_axes(self): """ Store x/y/z - limits and camera position """ try: self.azim = self.ax3d.azim self.elev = self.ax3d.elev self.dist = self.ax3d.dist self.xlim = self.ax3d.get_xlim3d() self.ylim = self.ax3d.get_ylim3d() self.zlim = self.ax3d.get_zlim3d() except AttributeError: # not yet initialized, set standard values self.azim = -65 self.elev = 30 self.dist = 10 self.xlim = (self.xmin, self.xmax) self.ylim = (self.ymin, self.ymax) self.zlim = (self.zmin, self.zmax) # ------------------------------------------------------------------------------ def _restore_axes(self): """ Restore x/y/z - limits and camera position """ if self.mplwidget.mplToolbar.a_lk.isChecked(): self.ax3d.set_xlim3d(self.xlim) self.ax3d.set_ylim3d(self.ylim) self.ax3d.set_zlim3d(self.zlim) self.ax3d.azim = self.azim self.ax3d.elev = self.elev self.ax3d.dist = self.dist # ------------------------------------------------------------------------------ def _log_clicked(self): """ Change scale and settings to log / lin when log setting is changed Update min / max settings when lineEdits have been edited """ if self.sender().objectName( ) == 'but_log': # clicking but_log triggered the slot if self.but_log.isChecked(): self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = np.round(20 * log10(self.zmax), 2) self.ledTop.setText(str(self.zmax_dB)) self.lblTopdB.setVisible(True) self.lblBottomdB.setVisible(True) else: self.ledBottom.setText(str(self.zmin)) self.zmax = np.round(10**(self.zmax_dB / 20), 2) self.ledTop.setText(str(self.zmax)) self.lblTopdB.setVisible(False) self.lblBottomdB.setVisible(False) else: # finishing a lineEdit field triggered the slot if self.but_log.isChecked(): self.zmin_dB = safe_eval(self.ledBottom.text(), self.zmin_dB, return_type='float') self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = safe_eval(self.ledTop.text(), self.zmax_dB, return_type='float') self.ledTop.setText(str(self.zmax_dB)) else: self.zmin = safe_eval(self.ledBottom.text(), self.zmin, return_type='float') self.ledBottom.setText(str(self.zmin)) self.zmax = safe_eval(self.ledTop.text(), self.zmax, return_type='float') self.ledTop.setText(str(self.zmax)) self.draw() # ------------------------------------------------------------------------------ def draw(self): """ Main drawing entry point: perform the actual plot """ self.draw_3d() # ------------------------------------------------------------------------------ def draw_3d(self): """ Draw various 3D plots """ self.init_axes() bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] zz = np.array(fb.fil[0]['zpk'][0]) pp = np.array(fb.fil[0]['zpk'][1]) wholeF = fb.fil[0]['freqSpecsRangeType'] != 'half' # not used f_S = fb.fil[0]['f_S'] N_FFT = params['N_FFT'] alpha = self.diaAlpha.value() / 10. cmap = cm.get_cmap(str(self.cmbColormap.currentText())) if self.but_colormap_r.isChecked(): cmap = cmap.reversed() # use reversed colormap # Number of Lines /step size for H(f) stride, mesh, contour3d: stride = 10 - self.diaHatch.value() NL = 3 * self.diaHatch.value() + 5 surf_enabled = qget_cmb_box(self.cmbMode3D, data=False) in {'Surf', 'Contour'}\ or self.but_contour_2d.isChecked() self.cmbColormap.setEnabled(surf_enabled) self.but_colormap_r.setEnabled(surf_enabled) self.but_lighting.setEnabled(surf_enabled) self.but_colbar.setEnabled(surf_enabled) self.diaAlpha.setEnabled(surf_enabled or self.but_contour_2d.isChecked()) # cNorm = colors.Normalize(vmin=0, vmax=values[-1]) # scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet) # ----------------------------------------------------------------------------- # Calculate H(w) along the upper half of unity circle # ----------------------------------------------------------------------------- [w, H] = sig.freqz(bb, aa, worN=N_FFT, whole=True) H = np.nan_to_num(H) # replace nans and inf by finite numbers H_abs = abs(H) H_max = max(H_abs) H_min = min(H_abs) # f = w / (2 * pi) * f_S # translate w to absolute frequencies # F_min = f[np.argmin(H_abs)] plevel_rel = 1.05 # height of plotted pole position relative to zmax zlevel_rel = 0.1 # height of plotted zero position relative to zmax if self.but_log.isChecked(): # logarithmic scale # suppress "divide by zero in log10" warnings old_settings_seterr = np.seterr() np.seterr(divide='ignore') bottom = np.floor(max(self.zmin_dB, 20 * log10(H_min)) / 10) * 10 top = self.zmax_dB top_bottom = top - bottom zlevel = bottom - top_bottom * zlevel_rel if self.cmbMode3D.currentText( ) == 'None': # "Poleposition": H(f) plot only plevel_top = 2 * bottom - zlevel # height of displayed pole position plevel_btm = bottom else: plevel_top = top + top_bottom * (plevel_rel - 1) plevel_btm = top np.seterr(**old_settings_seterr) else: # linear scale bottom = max(self.zmin, H_min) # min. display value top = self.zmax # max. display value top_bottom = top - bottom # top = zmax_rel * H_max # calculate display top from max. of H(f) zlevel = bottom + top_bottom * zlevel_rel # height of displayed zero position if self.cmbMode3D.currentText( ) == 'None': # "Poleposition": H(f) plot only #H_max = np.clip(max(H_abs), 0, self.zmax) # make height of displayed poles same to zeros plevel_top = bottom + top_bottom * zlevel_rel plevel_btm = bottom else: plevel_top = plevel_rel * top plevel_btm = top # calculate H(jw)| along the unity circle and |H(z)|, each clipped # between bottom and top H_UC = H_mag(bb, aa, self.xy_UC, top, H_min=bottom, log=self.but_log.isChecked()) Hmag = H_mag(bb, aa, self.z, top, H_min=bottom, log=self.but_log.isChecked()) # =============================================================== # Plot Unit Circle (UC) # =============================================================== if self.plt_UC.isChecked(): # Plot unit circle and marker at (1,0): self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, ones(len(self.xy_UC)) * bottom, lw=2, color='k') self.ax3d.plot([0.97, 1.03], [0, 0], [bottom, bottom], lw=2, color='k') # =============================================================== # Plot ||H(f)| along unit circle as 3D-lineplot # =============================================================== if self.but_Hf.isChecked(): self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, H_UC, alpha=0.8, lw=4) # draw once more as dashed white line to improve visibility self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, H_UC, 'w--', lw=4) if stride < 10: # plot thin vertical line every stride points on the UC for k in range(len(self.xy_UC[::stride])): self.ax3d.plot([ self.xy_UC.real[::stride][k], self.xy_UC.real[::stride][k] ], [ self.xy_UC.imag[::stride][k], self.xy_UC.imag[::stride][k] ], [ np.ones(len(self.xy_UC[::stride]))[k] * bottom, H_UC[::stride][k] ], linewidth=1, color=(0.5, 0.5, 0.5)) # =============================================================== # Plot Poles and Zeros # =============================================================== if self.but_PZ.isChecked(): PN_SIZE = 8 # size of P/N symbols # Plot zero markers at |H(z_i)| = zlevel with "stems": self.ax3d.plot(zz.real, zz.imag, ones(len(zz)) * zlevel, 'o', markersize=PN_SIZE, markeredgecolor='blue', markeredgewidth=2.0, markerfacecolor='none') for k in range(len(zz)): # plot zero "stems" self.ax3d.plot([zz[k].real, zz[k].real], [zz[k].imag, zz[k].imag], [bottom, zlevel], linewidth=1, color='b') # Plot the poles at |H(z_p)| = plevel with "stems": self.ax3d.plot(np.real(pp), np.imag(pp), plevel_top, 'x', markersize=PN_SIZE, markeredgewidth=2.0, markeredgecolor='red') for k in range(len(pp)): # plot pole "stems" self.ax3d.plot([pp[k].real, pp[k].real], [pp[k].imag, pp[k].imag], [plevel_btm, plevel_top], linewidth=1, color='r') # =============================================================== # 3D-Plots of |H(z)| clipped between |H(z)| = top # =============================================================== m_cb = cm.ScalarMappable( cmap=cmap) # normalized proxy object that is mappable m_cb.set_array(Hmag) # for colorbar # --------------------------------------------------------------- # 3D-mesh plot # --------------------------------------------------------------- if self.cmbMode3D.currentText() == 'Mesh': # fig_mlab = mlab.figure(fgcolor=(0., 0., 0.), bgcolor=(1, 1, 1)) # self.ax3d.set_zlim(0,2) self.ax3d.plot_wireframe(self.x, self.y, Hmag, rstride=5, cstride=stride, linewidth=1, color='gray') # --------------------------------------------------------------- # 3D-surface plot # --------------------------------------------------------------- # http://stackoverflow.com/questions/28232879/phong-shading-for-shiny-python-3d-surface-plots elif self.cmbMode3D.currentText() == 'Surf': if MLAB: # Mayavi surf = mlab.surf(self.x, self.y, H_mag, colormap='RdYlBu', warp_scale='auto') # Change the visualization parameters. surf.actor.property.interpolation = 'phong' surf.actor.property.specular = 0.1 surf.actor.property.specular_power = 5 # s = mlab.contour_surf(self.x, self.y, Hmag, contour_z=0) mlab.show() else: if self.but_lighting.isChecked(): ls = LightSource(azdeg=0, altdeg=65) # Create light source object rgb = ls.shade( Hmag, cmap=cmap) # Shade data, creating an rgb array cmap_surf = None else: rgb = None cmap_surf = cmap # s = self.ax3d.plot_surface(self.x, self.y, Hmag, # alpha=OPT_3D_ALPHA, rstride=1, cstride=1, cmap=cmap, # linewidth=0, antialiased=False, shade=True, facecolors = rgb) # s.set_edgecolor('gray') s = self.ax3d.plot_surface(self.x, self.y, Hmag, alpha=alpha, rstride=1, cstride=1, linewidth=0, antialiased=False, facecolors=rgb, cmap=cmap_surf, shade=True) s.set_edgecolor(None) # --------------------------------------------------------------- # 3D-Contour plot # --------------------------------------------------------------- elif self.cmbMode3D.currentText() == 'Contour': s = self.ax3d.contourf3D(self.x, self.y, Hmag, NL, alpha=alpha, cmap=cmap) # --------------------------------------------------------------- # 2D-Contour plot # TODO: 2D contour plots do not plot correctly together with 3D plots in # current matplotlib 1.4.3 -> disable them for now # TODO: zdir = x / y delivers unexpected results -> rather plot max(H) # along the other axis? # TODO: colormap is created depending on the zdir = 'z' contour plot # -> set limits of (all) other plots manually? if self.but_contour_2d.isChecked(): # self.ax3d.contourf(x, y, Hmag, 20, zdir='x', offset=xmin, # cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom) # self.ax3d.contourf(x, y, Hmag, 20, zdir='y', offset=ymax, # cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom) s = self.ax3d.contourf(self.x, self.y, Hmag, NL, zdir='z', offset=bottom - (top - bottom) * 0.05, cmap=cmap, alpha=alpha) # plot colorbar for suitable plot modes if self.but_colbar.isChecked() and ( self.but_contour_2d.isChecked() or str(self.cmbMode3D.currentText()) in {'Contour', 'Surf'}): self.colb = self.mplwidget.fig.colorbar(m_cb, ax=self.ax3d, shrink=0.8, aspect=20, pad=0.02, fraction=0.08) # ---------------------------------------------------------------------- # Set view limits and labels # ---------------------------------------------------------------------- if not self.mplwidget.mplToolbar.a_lk.isChecked(): self.ax3d.set_xlim3d(self.xmin, self.xmax) self.ax3d.set_ylim3d(self.ymin, self.ymax) self.ax3d.set_zlim3d(bottom, top) else: self._restore_axes() self.ax3d.set_xlabel('Re') #(fb.fil[0]['plt_fLabel']) self.ax3d.set_ylabel( 'Im' ) #(r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $') # self.ax3d.set_zlabel(r'$|H(z)|\; \rightarrow $') self.ax3d.set_title( r'3D-Plot of $|H(\mathrm{e}^{\mathrm{j} \Omega})|$ and $|H(z)|$') self.redraw() # ------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
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_Impz(QWidget): """ Construct a widget for plotting impulse and general transient responses """ # incoming sig_rx = pyqtSignal(object) # outgoing, e.g. when stimulus has been calculated sig_tx = pyqtSignal(object) def __init__(self, parent): super(Plot_Impz, self).__init__(parent) self.ACTIVE_3D = False self.ui = PlotImpz_UI(self) # create the UI part with buttons etc. # initial settings for line edit widgets self.f1 = self.ui.f1 self.f2 = self.ui.f2 self.bottom_t = self.ui.bottom_t self.bottom_f = self.ui.bottom_f self.needs_draw = True # flag whether plots need to be updated self.needs_redraw = [True] * 2 # flag which plot needs to be redrawn self.fx_sim = False # initial setting for fixpoint simulation self.tool_tip = "Impulse and transient response" self.tab_label = "h[n]" self.active_tab = 0 # index for active tab self._construct_UI() #-------------------------------------------- # initialize routines and settings self._log_mode_time() self._log_mode_freq() self.fx_select() # initialize fixpoint or float simulation self.draw() # initial calculation and drawing def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # Define MplWidgets: Time domain plots #---------------------------------------------------------------------- self.mplwidget_t = MplWidget(self) self.mplwidget_t.setObjectName("mplwidget_t1") self.mplwidget_t.layVMainMpl.addWidget(self.ui.wdg_ctrl_time) self.mplwidget_t.layVMainMpl.setContentsMargins(*params['wdg_margins']) # MplWidget for frequency domain plots self.mplwidget_f = MplWidget(self) self.mplwidget_f.setObjectName("mplwidget_f1") self.mplwidget_f.layVMainMpl.addWidget(self.ui.wdg_ctrl_freq) self.mplwidget_f.layVMainMpl.setContentsMargins(*params['wdg_margins']) #---------------------------------------------------------------------- # Tabbed layout with vertical tabs #---------------------------------------------------------------------- self.tabWidget = QTabWidget(self) self.tabWidget.addTab(self.mplwidget_t, "Time") self.tabWidget.addTab(self.mplwidget_f, "Frequency") # list with tabWidgets self.tab_mplwidgets = ["mplwidget_t", "mplwidget_f"] self.tabWidget.setTabPosition(QTabWidget.West) layVMain = QVBoxLayout() layVMain.addWidget(self.tabWidget) layVMain.addWidget(self.ui.wdg_ctrl_stim) layVMain.addWidget(self.ui.wdg_ctrl_run) layVMain.setContentsMargins(*params['wdg_margins'])#(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # --- run control --- self.ui.cmb_sim_select.currentIndexChanged.connect(self.fx_select) self.ui.but_run.clicked.connect(self.fx_run) self.ui.chk_fx_scale.clicked.connect(self.draw_impz_time) self.ui.chk_stim_options.clicked.connect(self._show_stim_options) # --- time domain plotting --- self.ui.cmb_plt_time_resp.currentIndexChanged.connect(self.draw_impz_time) self.ui.chk_mrk_time_resp.clicked.connect(self.draw_impz_time) self.ui.cmb_plt_time_stim.currentIndexChanged.connect(self.draw_impz_time) self.ui.chk_mrk_time_stim.clicked.connect(self.draw_impz_time) self.ui.chk_log_time.clicked.connect(self._log_mode_time) self.ui.led_log_bottom_time.editingFinished.connect(self._log_mode_time) self.ui.chk_fx_range.clicked.connect(self.draw_impz_time) self.ui.chk_win_time.clicked.connect(self.draw_impz_time) # --- frequency domain plotting --- self.ui.cmb_plt_freq_resp.currentIndexChanged.connect(self.draw_impz_freq) self.ui.chk_mrk_freq_resp.clicked.connect(self.draw_impz_freq) self.ui.cmb_plt_freq_stim.currentIndexChanged.connect(self.draw_impz_freq) self.ui.chk_mrk_freq_stim.clicked.connect(self.draw_impz_freq) self.ui.chk_log_freq.clicked.connect(self._log_mode_freq) self.ui.led_log_bottom_freq.editingFinished.connect(self._log_mode_freq) self.ui.chk_win_freq.clicked.connect(self.draw_impz_freq) # frequency widgets require special handling as they are scaled with f_s self.ui.ledFreq1.installEventFilter(self) self.ui.ledFreq2.installEventFilter(self) self.mplwidget_t.mplToolbar.sig_tx.connect(self.process_sig_rx) # connect to toolbar self.mplwidget_f.mplToolbar.sig_tx.connect(self.process_sig_rx) # connect to toolbar # When user has selected a different tab, trigger a draw (incl. maybe recalc) of current tab self.tabWidget.currentChanged.connect(self.draw) # passes number of active tab self.sig_rx.connect(self.ui.sig_rx) self.ui.sig_tx.connect(self.process_sig_rx) # connect to widgets and signals upstream #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and input_tab_widgets """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_draw, self.isVisible())) if dict_sig['sender'] == __name__: logger.warning("Stopped infinite loop, {0}".format(dict_sig)) if 'fx_sim' in dict_sig: try: if dict_sig['fx_sim'] == 'set_hdl_dict': self.fx_set_hdl_dict(dict_sig) # pass hdl dict if dict_sig['fx_sim'] == 'get_stimulus': # read hdl_dict and calculate stimulus self.hdl_dict = dict_sig['hdl_dict'] self.calc_stimulus() # calculate selected stimulus with selected length # pass stimulus in self.x back via dict self.sig_tx.emit({'sender':__name__, 'fx_sim':'set_stimulus', 'fx_stimulus':self.x}) elif dict_sig['fx_sim'] == 'set_results': logger.info("Received fixpoint results.") self.fx_set_results(dict_sig) # plot fx simulation results except KeyError as e: logger.error('Key {0} missing in "hdl_dict".'.format(e)) self.fx_sim = False if self.isVisible(): if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\ or 'view_changed' in dict_sig or self.needs_draw: # todo: after 'data_changed' all needs to be set to True except current widget self.draw() elif 'home' in dict_sig: self.redraw() # self.tabWidget.currentWidget().redraw() # redraw method of current mplwidget, always redraws tab 0 self.needs_redraw[self.tabWidget.currentIndex()] = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.needs_redraw[self.tabWidget.currentIndex()]: self.needs_redraw[:] = [True] * 3 self.redraw() # redraw current widget else: if 'data_changed' in dict_sig or 'specs_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.needs_redraw[:] = [True] * 3 #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the monitored widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (``QEvent.FocusIn``), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (``QEvent.FocusOut``), store current value normalized to f_S with full precision (only if ``spec_edited == True``) and display the stored value in selected format """ def _store_entry(source): if self.spec_edited: if source.objectName() == "stimFreq1": self.f1 = safe_eval(source.text(), self.f1 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": self.f2 = safe_eval(source.text(), self.f2 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) self.spec_edited = False # reset flag self.draw() # if isinstance(source, QLineEdit): # if source.objectName() in {"stimFreq1","stimFreq2"}: if event.type() in {QEvent.FocusIn,QEvent.KeyPress, QEvent.FocusOut}: if event.type() == QEvent.FocusIn: self.spec_edited = False self.load_fs() elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {Qt.Key_Return, Qt.Key_Enter}: _store_entry(source) elif key == Qt.Key_Escape: # revert changes self.spec_edited = False if source.objectName() == "stimFreq1": source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) elif event.type() == QEvent.FocusOut: _store_entry(source) # Call base class method to continue normal event processing: return super(Plot_Impz, self).eventFilter(source, event) #------------------------------------------------------------- def load_fs(self): """ Reload sampling frequency from filter dictionary and transform the displayed frequency spec input fields according to the units setting (i.e. f_S). Spec entries are always stored normalized w.r.t. f_S in the dictionary; when f_S or the unit are changed, only the displayed values of the frequency entries are updated, not the dictionary! load_fs() is called during init and when the frequency unit or the sampling frequency have been changed. It should be called when sigSpecsChanged or sigFilterDesigned is emitted at another place, indicating that a reload is required. """ # recalculate displayed freq spec values for (maybe) changed f_S if self.ui.ledFreq1.hasFocus(): # widget has focus, show full precision self.ui.ledFreq1.setText(str(self.f1 * fb.fil[0]['f_S'])) elif self.ui.ledFreq2.hasFocus(): # widget has focus, show full precision self.ui.ledFreq2.setText(str(self.f2 * fb.fil[0]['f_S'])) else: # widgets have no focus, round the display self.ui.ledFreq1.setText( str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) self.ui.ledFreq2.setText( str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) #------------------------------------------------------------- def _show_stim_options(self): """ Hide / show panel with stimulus options """ self.ui.wdg_ctrl_stim.setVisible(self.ui.chk_stim_options.isChecked()) # ============================================================================= def fx_select(self): """ Select between fixpoint and floating point simulation """ self.sim_select = qget_cmb_box(self.ui.cmb_sim_select, data=False) self.fx_sim = (self.sim_select == 'Fixpoint') self.ui.but_run.setVisible(self.fx_sim) self.ui.chk_fx_scale.setVisible(self.fx_sim) self.ui.chk_fx_range.setVisible(self.fx_sim) self.hdl_dict = None if self.fx_sim: qstyle_widget(self.ui.but_run, "changed") self.fx_run() else: self.draw() def fx_run(self): """ Run fixpoint simulation """ self.sig_tx.emit({'sender':__name__, 'fx_sim':'start'}) def fx_set_hdl_dict(self, dict_sig): """ Set quantization dict """ try: self.hdl_dict = dict_sig['hdl_dict'] except (KeyError, ValueError) as e: logger.warning(e) def fx_set_results(self, dict_sig): """ Get simulation results from `dict_sig` and transfer them to plotting routine. """ self.calc_response(dict_sig['fx_results']) qset_cmb_box(self.ui.cmb_sim_select, "Fixpoint", fireSignals=True) self.calc_fft() self.draw_impz() #------------------------------------------------------------------------------ def calc_stimulus(self): """ (Re-)calculate stimulus `self.x` """ self.n = np.arange(self.ui.N_end) self.t = self.n / fb.fil[0]['f_S'] # calculate stimuli x[n] ============================================== if self.ui.stim == "Pulse": self.x = np.zeros(self.ui.N_end) self.x[0] = self.ui.A1 # create dirac impulse as input signal self.title_str = r'Impulse Response' self.H_str = r'$h[n]$' # default elif self.ui.stim == "None": self.x = np.zeros(self.ui.N_end) self.title_str = r'System Response to Zero Input' self.H_str = r'$h_0[n]$' # default elif self.ui.stim == "Step": self.x = self.ui.A1 * np.ones(self.ui.N_end) # create step function self.title_str = r'Step Response' self.H_str = r'$h_{\epsilon}[n]$' elif self.ui.stim == "StepErr": self.x = self.ui.A1 * np.ones(self.ui.N_end) # create step function self.title_str = r'Settling Error' self.H_str = r'$h_{\epsilon, \infty} - h_{\epsilon}[n]$' elif self.ui.stim == "Cos": self.x = self.ui.A1 * np.cos(2 * np.pi * self.n * self.f1) +\ self.ui.A2 * np.cos(2 * np.pi * self.n * self.f2 + self.ui.phi2) self.title_str = r'System Response to Cosine Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Sine": self.x = self.ui.A1 * np.sin(2 * np.pi * self.n * self.f1 + self.ui.phi1) +\ self.ui.A2 * np.sin(2 * np.pi * self.n * self.f2 + self.ui.phi2) self.title_str = r'System Response to Sinusoidal Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Rect": self.x = self.ui.A1 * np.sign(np.sin(2 * np.pi * self.n * self.f1)) self.title_str = r'System Response to Rect. Signal' self.H_str = r'$y[n]$' elif self.ui.stim == "Saw": self.x = self.ui.A1 * sig.sawtooth(self.n * self.f1 * 2*np.pi) self.title_str = r'System Response to Sawtooth Signal' self.H_str = r'$y[n]$' else: logger.error('Unknown stimulus "{0}"'.format(self.ui.stim)) return # Add noise to stimulus if self.ui.noise == "gauss": self.x[self.ui.N_start:] += self.ui.noi * np.random.randn(self.ui.N) self.title_str += r' w/ Gaussian Noise' elif self.ui.noise == "uniform": self.x[self.ui.N_start:] += self.ui.noi * (np.random.rand(self.ui.N)-0.5) self.title_str += r' w/ Uniform Noise' # Add DC to stimulus when visible / enabled if self.ui.ledDC.isVisible: self.x += self.ui.DC if self.ui.DC != 0: self.title_str += r' and DC' if self.fx_sim: self.title_str = r'$Fixpoint$ ' + self.title_str self.needs_redraw[:] = [True] * 2 #------------------------------------------------------------------------------ def calc_response(self, y_fx = None): """ (Re-)calculate filter response `self.y` from either stimulus `self.x` (float mode) or copy fixpoint response. Split response into imag. and real components `self.y_i` and `self.y_r` and set the flag `self.cmplx`. """ if self.fx_sim: # use fixpoint simulation results instead of floating results if y_fx is not None: self.y = np.array(y_fx) qstyle_widget(self.ui.but_run, "normal") else: self.y = None else: # calculate response self.y_r[n] and self.y_i[n] (for complex case) ===== self.bb = np.asarray(fb.fil[0]['ba'][0]) self.aa = np.asarray(fb.fil[0]['ba'][1]) if min(len(self.aa), len(self.bb)) < 2: logger.error('No proper filter coefficients: len(a), len(b) < 2 !') return logger.info("Coefficient area = {0}".format(np.sum(np.abs(self.bb)))) sos = np.asarray(fb.fil[0]['sos']) antiCausal = 'zpkA' in fb.fil[0] causal = not (antiCausal) if len(sos) > 0 and causal: # has second order sections and is causal y = sig.sosfilt(sos, self.x) elif antiCausal: y = sig.filtfilt(self.bb, self.aa, self.x, -1, None) else: # no second order sections or antiCausals for current filter y = sig.lfilter(self.bb, self.aa, self.x) if self.ui.stim == "StepErr": dc = sig.freqz(self.bb, self.aa, [0]) # DC response of the system y = y - abs(dc[1]) # subtract DC (final) value from response self.y = np.real_if_close(y, tol = 1e3) # tol specified in multiples of machine eps self.needs_redraw[0] = True self.needs_redraw[1] = True # Calculate imag. and real components from response self.cmplx = np.any(np.iscomplex(self.y)) if self.cmplx: self.y_i = self.y.imag self.y_r = self.y.real else: self.y_r = self.y self.y_i = None #------------------------------------------------------------------------------ def calc_fft(self): """ (Re-)calculate ffts X(f) and Y(f) of stimulus and response """ #if self.plt_freq_resp != "none": # calculate FFT of stimulus / response if self.x is None or len(self.x) < self.ui.N_end: self.X = np.zeros(self.ui.N_end-self.ui.N_start) # dummy result if self.x is None: logger.warning("Stimulus is 'None', FFT cannot be calculated.") else: logger.warning("Length of stimulus is {0} < N = {1}, FFT cannot be calculated." .format(len(self.x), self.ui.N_end)) else: x_win = self.x[self.ui.N_start:self.ui.N_end] * self.ui.win self.X = np.abs(np.fft.fft(x_win)) / self.ui.N #if self.plt_freq_resp != "none": if self.y is None or len(self.y) < self.ui.N_end: self.Y = np.zeros(self.ui.N_end-self.ui.N_start) # dummy result if self.y is None: logger.warning("Transient response is 'None', FFT cannot be calculated.") else: logger.warning("Length of transient response is {0} < N = {1}, FFT cannot be calculated." .format(len(self.y), self.ui.N_end)) else: y_win = self.y[self.ui.N_start:self.ui.N_end] * self.ui.win self.Y = np.abs(np.fft.fft(y_win)) / self.ui.N if self.ui.chk_win_freq.isChecked(): self.Win = np.abs(np.fft.fft(self.ui.win)) / self.ui.N self.needs_redraw[1] = True #------------------------------------------------------------------------------ def update_view(self): """ Only update the limits without recalculating the impulse response """ self.draw_impz() ############################################################################### # PLOTTING ############################################################################### def draw(self): """ Recalculate response and redraw it """ if True: # self.needs_draw: doesn't work yet - number of data points needs to updated self.calc_stimulus() self.calc_response() self.needs_draw = False self.draw_impz() #------------------------------------------------------------------------------ def draw_impz(self): """ (Re-)draw the figure without recalculation """ if not hasattr(self, 'cmplx'): # has response been calculated yet? logger.error("self.y {0}".format(self.y)) self.calc_stimulus() self.calc_response() f_unit = fb.fil[0]['freq_specs_unit'] if f_unit in {"f_S", "f_Ny"}: unit_frmt = "i" # italic else: unit_frmt = None self.ui.lblFreqUnit1.setText(to_html(f_unit, frmt=unit_frmt)) self.ui.lblFreqUnit2.setText(to_html(f_unit, frmt=unit_frmt)) self.load_fs() #self.init_axes() self.fmt_plot_resp = {'color':'red', 'linewidth':2, 'alpha':0.5} self.fmt_mkr_resp = {'color':'red', 'alpha':0.5} self.fmt_plot_stim = {'color':'blue', 'linewidth':2, 'alpha':0.5} self.fmt_mkr_stim = {'color':'blue', 'alpha':0.5} self.fmt_stem_stim = params['mpl_stimuli'] idx = self.tabWidget.currentIndex() if idx == 0: self.draw_impz_time() elif idx == 1: self.draw_impz_freq() else: logger.error("Index {0} out of range!".format(idx)) #================ Plotting routine time domain ========================= def _log_mode_time(self): """ Select / deselect log. mode for time domain and update self.bottom_t """ log = self.ui.chk_log_time.isChecked() self.ui.lbl_log_bottom_time.setVisible(log) self.ui.led_log_bottom_time.setVisible(log) self.ui.lbl_dB_time.setVisible(log) if log: self.bottom_t = safe_eval(self.ui.led_log_bottom_time.text(), self.bottom_t, return_type='float', sign='neg') self.ui.led_log_bottom_time.setText(str(self.bottom_t)) else: self.bottom_t = 0 self.draw_impz() def _log_mode_freq(self): """ Select / deselect log. mode for frequency domain and update self.bottom_f """ log = self.ui.chk_log_freq.isChecked() self.ui.lbl_log_bottom_freq.setVisible(log) self.ui.led_log_bottom_freq.setVisible(log) self.ui.lbl_dB_freq.setVisible(log) if log: self.bottom_f = safe_eval(self.ui.led_log_bottom_freq.text(), self.bottom_f, return_type='float', sign='neg') self.ui.led_log_bottom_freq.setText(str(self.bottom_f)) else: self.bottom_f = 0 self.draw_impz() def plot_fnc(self, plt_style, ax, plt_dict=None, bottom=0): """ Return a plot method depending on the parameter `plt_style` (str) and the axis instance `ax`. An optional `plt_dict` is modified in place. """ if plt_dict is None: plt_dict = {} if plt_style == "line": plot_fnc = getattr(ax, "plot") elif plt_style == "stem": plot_fnc = stems plt_dict.update({'ax':ax, 'bottom':bottom}) elif plt_style == "step": plot_fnc = getattr(ax, "plot") plt_dict.update({'drawstyle':'steps-mid'}) elif plt_style == "dots": plot_fnc = getattr(ax, "scatter") elif plt_style == "none": plot_fnc = no_plot else: plot_fnc = no_plot return plot_fnc def _init_axes_time(self): """ Clear the axes of the time domain matplotlib widgets and (re)draw the plots. """ self.plt_time_stim = qget_cmb_box(self.ui.cmb_plt_time_stim, data=False).lower() self.plt_time_resp = qget_cmb_box(self.ui.cmb_plt_time_resp, data=False).lower() plt_time = self.plt_time_resp != "none" or self.plt_time_stim != "none" self.mplwidget_t.fig.clf() # clear figure with axes if plt_time: num_subplots = 1 + (self.cmplx and self.plt_time_resp != "none") self.mplwidget_t.fig.subplots_adjust(hspace = 0.5) self.ax_r = self.mplwidget_t.fig.add_subplot(num_subplots,1 ,1) self.ax_r.clear() self.ax_r.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_r.get_yaxis().tick_left() # remove axis ticks right if self.cmplx and self.plt_time_resp != "none": self.ax_i = self.mplwidget_t.fig.add_subplot(num_subplots, 1, 2, sharex = self.ax_r) self.ax_i.clear() self.ax_i.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_i.get_yaxis().tick_left() # remove axis ticks right if self.ACTIVE_3D: # not implemented / tested yet self.ax3d = self.mplwidget_t.fig.add_subplot(111, projection='3d') def draw_impz_time(self): """ (Re-)draw the time domain mplwidget """ if self.y is None: # safety net for empty responses for ax in self.mplwidget_t.fig.get_axes(): # remove all axes self.mplwidget_t.fig.delaxes(ax) return mkfmt_i = 'd' self._init_axes_time() scale_i = scale_o = 1 fx_min = -1. fx_max = 1. fx_title = "" if self.fx_sim: # fixpoint simulation enabled -> scale stimulus and response WI = WO = 1 try: logger.info("hdl_dict = {0}".format(self.hdl_dict)) WI = self.hdl_dict['QI']['W'] WO = self.hdl_dict['QO']['W'] except AttributeError as e: logger.error("Attribute error: {0}".format(e)) except TypeError as e: logger.error("Type error: 'hdl_dict'={0},\n{1}".format(self.hdl_dict, e)) except ValueError as e: logger.error("Value error: {0}".format(e)) if self.ui.chk_fx_scale.isChecked(): scale_i = 1 << WI-1 fx_min = - (1 << WO-1) fx_max = (1 << WO-1) - 1 else: scale_o = 1. / (1 << WO-1) fx_min = -1 fx_max = 1 - scale_o logger.info("scale I:{0} O:{1}".format(scale_i, scale_o)) if self.ui.chk_log_time.isChecked(): # log. scale for stimulus / response time domain H_str = '$|$' + self.H_str + '$|$ in dBV' x = np.maximum(20 * np.log10(abs(self.x * scale_i)), self.bottom_t) y = np.maximum(20 * np.log10(abs(self.y_r * scale_o)), self.bottom_t) win = np.maximum(20 * np.log10(abs(self.ui.win)), self.bottom_t) if self.cmplx: y_i = np.maximum(20 * np.log10(abs(self.y_i)), self.bottom_t) H_i_str = r'$|\Im\{$' + self.H_str + '$\}|$' + ' in dBV' H_str = r'$|\Re\{$' + self.H_str + '$\}|$' + ' in dBV' fx_min = 20*np.log10(abs(fx_min)) fx_max = fx_min else: x = self.x * scale_i y = self.y_r * scale_o win = self.ui.win if self.cmplx: y_i = self.y_i * scale_o if self.cmplx: H_i_str = r'$\Im\{$' + self.H_str + '$\}$ in V' H_str = r'$\Re\{$' + self.H_str + '$\}$ in V' else: H_str = self.H_str + ' in V' if self.ui.chk_fx_range.isChecked() and self.fx_sim: self.ax_r.axhline(fx_max,0, 1, color='k', linestyle='--') self.ax_r.axhline(fx_min,0, 1, color='k', linestyle='--') # --------------- Stimulus plot ---------------------------------- plot_stim_dict = self.fmt_plot_stim.copy() plot_stim_fnc = self.plot_fnc(self.plt_time_stim, self.ax_r, plot_stim_dict, self.bottom_t) plot_stim_fnc(self.t[self.ui.N_start:], x[self.ui.N_start:], label='$x[n]$', **plot_stim_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_stim.isChecked() and self.plt_time_stim not in {"dots","none"}: self.ax_r.scatter(self.t[self.ui.N_start:], x[self.ui.N_start:], **self.fmt_mkr_stim) # --------------- Response plot ---------------------------------- plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_time_resp, self.ax_r, plot_resp_dict, self.bottom_t) plot_resp_fnc(self.t[self.ui.N_start:], y[self.ui.N_start:], label='$y[n]$', **plot_resp_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_resp.isChecked() and self.plt_time_resp not in {"dots","none"}: self.ax_r.scatter(self.t[self.ui.N_start:], y[self.ui.N_start:], **self.fmt_mkr_resp) # --------------- Window plot ---------------------------------- if self.ui.chk_win_time.isChecked(): self.ax_r.plot(self.t[self.ui.N_start:], win, c="gray", label=self.ui.window_type) self.ax_r.legend(loc='best', fontsize = 'small', fancybox=True, framealpha=0.5) # --------------- Complex response ---------------------------------- if self.cmplx and self.plt_time_resp != "none": #plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_time_resp, self.ax_i, plot_resp_dict, self.bottom_t) plot_resp_fnc(self.t[self.ui.N_start:], y_i[self.ui.N_start:], label='$y_i[n]$', **plot_resp_dict) # Add plot markers, this is way faster than normal stem plotting if self.ui.chk_mrk_time_resp.isChecked() and self.plt_time_resp not in {"dots","none"}: self.ax_i.scatter(self.t[self.ui.N_start:], y_i[self.ui.N_start:], marker=mkfmt_i, **self.fmt_mkr_resp) # [ml_i, sl_i, bl_i] = self.ax_i.stem(self.t[self.ui.N_start:], y_i[self.ui.N_start:], # bottom=self.bottom_t, markerfmt=mkfmt_i, label = '$y_i[n]$') # self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel']) # self.ax_r.get_xaxis().set_ticklabels([]) # removes both xticklabels # plt.setp(ax_r.get_xticklabels(), visible=False) # is shorter but imports matplotlib, set property directly instead: [label.set_visible(False) for label in self.ax_r.get_xticklabels()] self.ax_r.set_ylabel(H_str + r'$\rightarrow $') self.ax_i.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_i.set_ylabel(H_i_str + r'$\rightarrow $') self.ax_i.legend(loc='best', fontsize = 'small', fancybox=True, framealpha=0.5) else: self.ax_r.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_r.set_ylabel(H_str + r'$\rightarrow $') self.ax_r.set_title(fx_title + self.title_str) self.ax_r.set_xlim([self.t[self.ui.N_start],self.t[self.ui.N_end-1]]) expand_lim(self.ax_r, 0.02) if self.ACTIVE_3D: # not implemented / tested yet # plotting the stems for i in range(self.ui.N_start, self.ui.N_end): self.ax3d.plot([self.t[i], self.t[i]], [y[i], y[i]], [0, y_i[i]], '-', linewidth=2, alpha=.5) # plotting a circle on the top of each stem self.ax3d.plot(self.t[self.ui.N_start:], y[self.ui.N_start:], y_i[self.ui.N_start:], 'o', markersize=8, markerfacecolor='none', label='$y[n]$') self.ax3d.set_xlabel('x') self.ax3d.set_ylabel('y') self.ax3d.set_zlabel('z') self.redraw() # redraw currently active mplwidget #-------------------------------------------------------------------------- def _init_axes_freq(self): """ Clear the axes of the frequency domain matplotlib widgets and calculate the fft """ self.plt_freq_stim = qget_cmb_box(self.ui.cmb_plt_freq_stim, data=False).lower() self.plt_freq_resp = qget_cmb_box(self.ui.cmb_plt_freq_resp, data=False).lower() self.plt_freq_disabled = self.plt_freq_stim == "none" and self.plt_freq_resp == "none" if not self.ui.chk_log_freq.isChecked() and len(self.mplwidget_f.fig.get_axes()) == 2: self.mplwidget_f.fig.clear() # get rid of second axis when returning from log mode by clearing all if len(self.mplwidget_f.fig.get_axes()) == 0: # empty figure, no axes self.ax_fft = self.mplwidget_f.fig.add_subplot(111) self.ax_fft.get_xaxis().tick_bottom() # remove axis ticks on top self.ax_fft.get_yaxis().tick_left() # remove axis ticks right self.ax_fft.set_title("FFT of Transient Response") for ax in self.mplwidget_f.fig.get_axes(): # clear but don't delete all axes ax.cla() if self.ui.chk_log_freq.isChecked() and len(self.mplwidget_f.fig.get_axes()) == 1: # create second axis scaled for noise power scale if it doesn't exist yet self.ax_fft_noise = self.ax_fft.twinx() self.ax_fft_noise.is_twin = True self.calc_fft() def draw_impz_freq(self): """ (Re-)draw the frequency domain mplwidget """ self._init_axes_freq() plt_response = self.plt_freq_resp != "none" plt_stimulus = self.plt_freq_stim != "none" #if self.plt_freq != "None": if not self.plt_freq_disabled: if plt_response and not plt_stimulus: XY_str = r'$|Y(\mathrm{e}^{\mathrm{j} \Omega})|$' elif not plt_response and plt_stimulus: XY_str = r'$|X(\mathrm{e}^{\mathrm{j} \Omega})|$' else: XY_str = r'$|X,Y(\mathrm{e}^{\mathrm{j} \Omega})|$' F = np.fft.fftfreq(self.ui.N, d = 1. / fb.fil[0]['f_S']) if plt_stimulus: X = self.X.copy()/np.sqrt(2) # enforce deep copy and convert to RMS self.Px = np.sum(np.square(self.X)) if fb.fil[0]['freqSpecsRangeType'] == 'half': X[1:] = 2 * X[1:] # correct for single-sided spectrum (except DC) if plt_response: Y = self.Y.copy()/np.sqrt(2) # enforce deep copy and convert to RMS self.Py = np.sum(np.square(self.Y)) if fb.fil[0]['freqSpecsRangeType'] == 'half': Y[1:] = 2 * Y[1:] # correct for single-sided spectrum (except DC) if self.ui.chk_win_freq.isChecked(): Win = self.Win.copy()/np.sqrt(2) # enforce deep copy and convert to RMS if fb.fil[0]['freqSpecsRangeType'] == 'half': Win[1:] = 2 * Win[1:] # correct for single-sided spectrum (except DC) if self.ui.chk_log_freq.isChecked(): unit = unit_P = "dBW" unit_nenbw = "dB" nenbw = 10 * np.log10(self.ui.nenbw) if plt_stimulus: X = np.maximum(20 * np.log10(X), self.ui.bottom_f) self.Px = 10*np.log10(self.Px) if plt_response: Y = np.maximum(20 * np.log10(Y), self.ui.bottom_f) self.Py = 10*np.log10(self.Py) if self.ui.chk_win_freq.isChecked(): Win = np.maximum(20 * np.log10(Win), self.ui.bottom_f) else: unit = "Vrms" unit_P = "W" unit_nenbw = "bins" nenbw = self.ui.nenbw XY_str = XY_str + ' in ' + unit if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift X, Y and F by f_S/2 if plt_response: Y = np.fft.fftshift(Y) if plt_stimulus: X = np.fft.fftshift(X) if self.ui.chk_win_freq.isChecked(): Win = np.fft.fftshift(Win) F = np.fft.fftshift(F) elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of X, Y and F if plt_response: Y = Y[0:self.ui.N//2] if plt_stimulus: X = X[0:self.ui.N//2] if self.ui.chk_win_freq.isChecked(): Win = Win[0:self.ui.N//2] F = F[0:self.ui.N//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # plot for F = 0 ... 1 F = np.fft.fftshift(F) + fb.fil[0]['f_S']/2. # --------------- Plot stimulus and response ---------------------- labels = [] if plt_stimulus: plot_stim_dict = self.fmt_plot_stim.copy() plot_stim_fnc = self.plot_fnc(self.plt_freq_stim, self.ax_fft, plot_stim_dict, self.bottom_f) plot_stim_fnc(F, X, label='$Stim.$',**plot_stim_dict) if self.ui.chk_mrk_freq_stim.isChecked() and self.plt_freq_stim not in {"dots","none"}: self.ax_fft.scatter(F, X, **self.fmt_mkr_stim) labels.append("$P_X$ = {0:.3g} {1}".format(self.Px, unit_P)) if plt_response: plot_resp_dict = self.fmt_plot_resp.copy() plot_resp_fnc = self.plot_fnc(self.plt_freq_resp, self.ax_fft, plot_resp_dict, self.bottom_f) plot_resp_fnc(F, Y, label='$Resp.$',**plot_resp_dict) if self.ui.chk_mrk_freq_resp.isChecked() and self.plt_freq_resp not in {"dots","none"}: self.ax_fft.scatter(F, Y, **self.fmt_mkr_resp) labels.append("$P_Y$ = {0:.3g} {1}".format(self.Py, unit_P)) if self.ui.chk_win_freq.isChecked(): self.ax_fft.plot(F, Win, c="gray", label="win") labels.append("{0}".format(self.ui.window_type)) labels.append("$NENBW$ = {0:.4g} {1}".format(nenbw, unit_nenbw)) labels.append("$CGAIN$ = {0:.4g}".format(self.ui.scale)) # collect all plot objects, hope that the order isn't messed up and add two dummy handles # for the NENBW and the CGAIN labels handles = self.ax_fft.get_lines() handles.append(mpl_patches.Rectangle((0, 0), 1, 1, fc="white",ec="white", lw=0, alpha=0)) handles.append(mpl_patches.Rectangle((0, 0), 1, 1, fc="white",ec="white", lw=0, alpha=0)) self.ax_fft.legend(handles, labels, loc='best', fontsize = 'small', fancybox=True, framealpha=0.5) self.ax_fft.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_fft.set_ylabel(XY_str) self.ax_fft.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_fft.set_title(self.title_str) if self.ui.chk_log_freq.isChecked(): # scale second axis for noise power corr = 10*np.log10(self.ui.N / self.ui.nenbw) mn, mx = self.ax_fft.get_ylim() self.ax_fft_noise.set_ylim(mn+corr, mx+corr) self.ax_fft_noise.set_ylabel(r'$P_N$ in dBW') self.redraw() # redraw currently active mplwidget #------------------------------------------------------------------------------ def redraw(self): """ Redraw the currently visible canvas when e.g. the canvas size has changed """ idx = self.tabWidget.currentIndex() self.tabWidget.currentWidget().redraw() #wdg = getattr(self, self.tab_mplwidgets[idx]) logger.debug("Redrawing tab {0}".format(idx)) #wdg_cur.redraw() self.needs_redraw[idx] = False
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)
class Plot_3D(QWidget): """ Class for various 3D-plots: - lin / log line plot of H(f) - lin / log surf plot of H(z) - optional display of poles / zeros """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self, parent): super(Plot_3D, self).__init__(parent) self.zmin = 0 self.zmax = 4 self.zmin_dB = -80 self.cmap_default = 'RdYlBu_r' self.data_changed = True # flag whether data has changed self.size_changed = True # flag whether widget has been resized self.tool_tip = "3D magnitude response |H(z)|" self.tab_label = "3D" self._construct_UI() #------------------------------------------------------------------------------ def process_signals(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from ``sig_rx`` """ logger.debug("Processing {0} | data_changed = {1}, visible = {2}"\ .format(dict_sig, self.data_changed, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.data_changed: self.draw() self.data_changed = False self.size_changed = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.size_changed: self.redraw() self.size_changed = False else: if 'data_changed' in dict_sig: self.data_changed = True if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.size_changed = True #------------------------------------------------------------------------------ def _construct_UI(self): self.chkLog = QCheckBox("Log.", self) self.chkLog.setObjectName("chkLog") self.chkLog.setToolTip("Logarithmic scale") self.chkLog.setChecked(False) self.chkPolar = QCheckBox("Polar", self) self.chkPolar.setObjectName("chkPolar") self.chkPolar.setToolTip("Polar coordinate range") self.chkPolar.setChecked(False) self.lblBottom = QLabel("Bottom:", self) self.ledBottom = QLineEdit(self) self.ledBottom.setObjectName("ledBottom") self.ledBottom.setText(str(self.zmin)) self.ledBottom.setToolTip("Minimum display value.") self.lblBottomdB = QLabel("dB", self) self.lblBottomdB.setVisible(self.chkLog.isChecked()) self.lblTop = QLabel("Top:", self) self.ledTop = QLineEdit(self) self.ledTop.setObjectName("ledTop") self.ledTop.setText(str(self.zmax)) self.ledTop.setToolTip("Maximum display value.") self.lblTopdB = QLabel("dB", self) self.lblTopdB.setVisible(self.chkLog.isChecked()) self.chkUC = QCheckBox("UC", self) self.chkUC.setObjectName("chkUC") self.chkUC.setToolTip("Plot unit circle") self.chkUC.setChecked(True) self.chkPZ = QCheckBox("P/Z", self) self.chkPZ.setObjectName("chkPZ") self.chkPZ.setToolTip("Plot poles and zeros") self.chkPZ.setChecked(True) self.chkHf = QCheckBox("H(f)", self) self.chkHf.setObjectName("chkHf") self.chkHf.setToolTip("Plot H(f) along the unit circle") self.chkHf.setChecked(True) modes = ['None', 'Mesh', 'Surf', 'Contour'] self.cmbMode3D = QComboBox(self) self.cmbMode3D.addItems(modes) self.cmbMode3D.setObjectName("cmbShow3D") self.cmbMode3D.setToolTip("Select 3D-plot mode.") self.cmbMode3D.setCurrentIndex(0) self.cmbMode3D.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkColormap_r = QCheckBox("reverse", self) self.chkColormap_r.setToolTip("reverse colormap") self.chkColormap_r.setChecked(True) self.cmbColormap = QComboBox(self) self._init_cmb_colormap() self.cmbColormap.setToolTip("Select colormap") self.chkColBar = QCheckBox("Colorbar", self) self.chkColBar.setObjectName("chkColBar") self.chkColBar.setToolTip("Show colorbar") self.chkColBar.setChecked(False) self.chkLighting = QCheckBox("Lighting", self) self.chkLighting.setObjectName("chkLighting") self.chkLighting.setToolTip("Enable light source") self.chkLighting.setChecked(False) self.lblAlpha = QLabel("Alpha", self) self.diaAlpha = QDial(self) self.diaAlpha.setRange(0., 10.) self.diaAlpha.setValue(10) self.diaAlpha.setTracking(False) # produce less events when turning self.diaAlpha.setFixedHeight(30) self.diaAlpha.setFixedWidth(30) self.diaAlpha.setWrapping(False) self.diaAlpha.setToolTip("<span>Set transparency for surf and contour plots.</span>") self.lblHatch = QLabel("Stride", self) self.diaHatch = QDial(self) self.diaHatch.setRange(0., 9.) self.diaHatch.setValue(5) self.diaHatch.setTracking(False) # produce less events when turning self.diaHatch.setFixedHeight(30) self.diaHatch.setFixedWidth(30) self.diaHatch.setWrapping(False) self.diaHatch.setToolTip("Set line density for various plots.") self.chkContour2D = QCheckBox("Contour2D", self) self.chkContour2D.setObjectName("chkContour2D") self.chkContour2D.setToolTip("Plot 2D-contours at z =0") self.chkContour2D.setChecked(False) #---------------------------------------------------------------------- # LAYOUT for UI widgets #---------------------------------------------------------------------- layGControls = QGridLayout() layGControls.addWidget(self.chkLog, 0, 0) layGControls.addWidget(self.chkPolar, 1, 0) layGControls.addWidget(self.lblTop, 0, 2) layGControls.addWidget(self.ledTop, 0, 4) layGControls.addWidget(self.lblTopdB, 0, 5) layGControls.addWidget(self.lblBottom, 1, 2) layGControls.addWidget(self.ledBottom, 1, 4) layGControls.addWidget(self.lblBottomdB, 1, 5) layGControls.setColumnStretch(5,1) layGControls.addWidget(self.chkUC, 0, 6) layGControls.addWidget(self.chkHf, 1, 6) layGControls.addWidget(self.chkPZ, 0, 8) layGControls.addWidget(self.cmbMode3D, 0, 10) layGControls.addWidget(self.chkContour2D, 1, 10) layGControls.addWidget(self.cmbColormap, 0,12,1,1) layGControls.addWidget(self.chkColormap_r, 1,12) layGControls.addWidget(self.chkLighting, 0, 14) layGControls.addWidget(self.chkColBar, 1, 14) layGControls.addWidget(self.lblAlpha, 0, 15) layGControls.addWidget(self.diaAlpha, 0, 16) layGControls.addWidget(self.lblHatch, 1, 15) layGControls.addWidget(self.diaHatch, 1, 16) # This widget encompasses all control subwidgets self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layGControls) #---------------------------------------------------------------------- # mplwidget #---------------------------------------------------------------------- # This is the plot pane widget, encompassing the other widgets self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self._init_grid() # initialize grid and do initial plot #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkLog.clicked.connect(self._log_clicked) self.ledBottom.editingFinished.connect(self._log_clicked) self.ledTop.editingFinished.connect(self._log_clicked) self.chkPolar.clicked.connect(self._init_grid) self.chkUC.clicked.connect(self.draw) self.chkHf.clicked.connect(self.draw) self.chkPZ.clicked.connect(self.draw) self.cmbMode3D.currentIndexChanged.connect(self.draw) self.chkColBar.clicked.connect(self.draw) self.cmbColormap.currentIndexChanged.connect(self.draw) self.chkColormap_r.clicked.connect(self._init_cmb_colormap) self.chkLighting.clicked.connect(self.draw) self.diaAlpha.valueChanged.connect(self.draw) self.diaHatch.valueChanged.connect(self.draw) self.chkContour2D.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) #self.mplwidget.mplToolbar.enable_plot(state = False) # disable initially #------------------------------------------------------------------------------ def _init_cmb_colormap(self): """ (Re-)Load combobox with available colormaps""" if self.chkColormap_r.isChecked(): cmap_list = [m for m in cm.datad if m.endswith("_r")] else: cmap_list = [m for m in cm.datad if not m.endswith("_r")] # *_r colormaps reverse the color order cmap_list.sort() self.cmbColormap.blockSignals(True) # don't send signal "indexChanged" self.cmbColormap.clear() self.cmbColormap.addItems(cmap_list) self.cmbColormap.blockSignals(False) idx = self.cmbColormap.findText(self.cmap_default) if idx == -1: idx = 0 self.cmbColormap.setCurrentIndex(idx) #------------------------------------------------------------------------------ def _init_grid(self): """ Initialize (x,y,z) coordinate grid + (re)draw plot.""" phi_UC = np.linspace(0, 2*pi, 400, endpoint=True) # angles for unit circle self.xy_UC = np.exp(1j * phi_UC) # x,y coordinates of unity circle steps = 100 # number of steps for x, y, r, phi # self.xmin = -1.5; self.xmax = 1.5 # cartesian range limits self.ymin = -1.5; self.ymax = 1.5 rmin = 0; rmax = self.xmin # polar range limits # Calculate grids for 3D-Plots dr = rmax / steps * 2 # grid size for polar range dx = (self.xmax - self.xmin) / steps dy = (self.ymax - self.ymin) / steps # grid size cartesian range if self.chkPolar.isChecked(): # # Plot circular range in 3D-Plot [r, phi] = np.meshgrid(np.arange(rmin, rmax, dr), np.linspace(0, 2 * pi, steps, endpoint=True)) self.x = r * cos(phi) self.y = r * sin(phi) else: # cartesian grid [self.x, self.y] = np.meshgrid(np.arange(self.xmin, self.xmax, dx), np.arange(self.ymin, self.ymax, dy)) self.z = self.x + 1j*self.y # create coordinate grid for complex plane self.draw() # initial plot #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes to get rid of colorbar The azimuth / elevation / distance settings of the camera are restored after clearing the axes. See http://stackoverflow.com/questions/4575588/matplotlib-3d-plot-with-pyqt4-in-qtabwidget-mplwidget """ self._save_axes() self.mplwidget.fig.clf() # needed to get rid of colorbar self.ax3d = self.mplwidget.fig.add_subplot(111, projection='3d') self._restore_axes() #------------------------------------------------------------------------------ def _save_axes(self): """ Store x/y/z - limits and camera position """ try: self.azim = self.ax3d.azim self.elev = self.ax3d.elev self.dist = self.ax3d.dist self.xlim = self.ax3d.get_xlim3d() self.ylim = self.ax3d.get_ylim3d() self.zlim = self.ax3d.get_zlim3d() except AttributeError: # not yet initialized, set standard values self.azim = -65 self.elev = 30 self.dist = 10 self.xlim = (self.xmin, self.xmax) self.ylim = (self.ymin, self.ymax) self.zlim = (self.zmin, self.zmax) #------------------------------------------------------------------------------ def _restore_axes(self): """ Restore x/y/z - limits and camera position """ if self.mplwidget.mplToolbar.a_lk.isChecked(): self.ax3d.set_xlim3d(self.xlim) self.ax3d.set_ylim3d(self.ylim) self.ax3d.set_zlim3d(self.zlim) self.ax3d.azim = self.azim self.ax3d.elev = self.elev self.ax3d.dist = self.dist #------------------------------------------------------------------------------ def _log_clicked(self): """ Change scale and settings to log / lin when log setting is changed Update min / max settings when lineEdits have been edited """ self.log = self.chkLog.isChecked() if self.sender().objectName() == 'chkLog': # clicking chkLog triggered the slot if self.log: self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = np.round(20 * log10(self.zmax), 2) self.ledTop.setText(str(self.zmax_dB)) self.lblTopdB.setVisible(True) self.lblBottomdB.setVisible(True) else: self.ledBottom.setText(str(self.zmin)) self.zmax = np.round(10**(self.zmax_dB / 20), 2) self.ledTop.setText(str(self.zmax)) self.lblTopdB.setVisible(False) self.lblBottomdB.setVisible(False) else: # finishing a lineEdit field triggered the slot if self.log: self.zmin_dB = safe_eval(self.ledBottom.text(), self.zmin_dB, return_type='float') self.ledBottom.setText(str(self.zmin_dB)) self.zmax_dB = safe_eval(self.ledTop.text(), self.zmax_dB, return_type='float') self.ledTop.setText(str(self.zmax_dB)) else: self.zmin = safe_eval(self.ledBottom.text(), self.zmin, return_type='float') self.ledBottom.setText(str(self.zmin)) self.zmax = safe_eval(self.ledTop.text(), self.zmax, return_type='float') self.ledTop.setText(str(self.zmax)) self.draw() #------------------------------------------------------------------------------ def draw(self): """ Main drawing entry point: perform the actual plot """ self.draw_3d() #------------------------------------------------------------------------------ def draw_3d(self): """ Draw various 3D plots """ self.init_axes() bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] zz = np.array(fb.fil[0]['zpk'][0]) pp = np.array(fb.fil[0]['zpk'][1]) wholeF = fb.fil[0]['freqSpecsRangeType'] != 'half' # not used f_S = fb.fil[0]['f_S'] N_FFT = params['N_FFT'] alpha = self.diaAlpha.value()/10. cmap = cm.get_cmap(str(self.cmbColormap.currentText())) # Number of Lines /step size for H(f) stride, mesh, contour3d: stride = 10 - self.diaHatch.value() NL = 3 * self.diaHatch.value() + 5 surf_enabled = qget_cmb_box(self.cmbMode3D, data=False) in {'Surf', 'Contour'} self.cmbColormap.setEnabled(surf_enabled) self.chkColormap_r.setEnabled(surf_enabled) self.chkLighting.setEnabled(surf_enabled) self.chkColBar.setEnabled(surf_enabled) self.diaAlpha.setEnabled(surf_enabled or self.chkContour2D.isChecked()) #cNorm = colors.Normalize(vmin=0, vmax=values[-1]) #scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet) #----------------------------------------------------------------------------- # Calculate H(w) along the upper half of unity circle #----------------------------------------------------------------------------- [w, H] = sig.freqz(bb, aa, worN=N_FFT, whole=True) H = np.nan_to_num(H) # replace nans and inf by finite numbers H_abs = abs(H) H_max = max(H_abs) H_min = min(H_abs) #f = w / (2 * pi) * f_S # translate w to absolute frequencies #F_min = f[np.argmin(H_abs)] plevel_rel = 1.05 # height of plotted pole position relative to zmax zlevel_rel = 0.1 # height of plotted zero position relative to zmax if self.chkLog.isChecked(): # logarithmic scale bottom = np.floor(max(self.zmin_dB, 20*log10(H_min)) / 10) * 10 top = self.zmax_dB top_bottom = top - bottom zlevel = bottom - top_bottom * zlevel_rel if self.cmbMode3D.currentText() == 'None': # "Poleposition" for H(f) plot only plevel_top = 2 * bottom - zlevel # height of displayed pole position plevel_btm = bottom else: plevel_top = top + top_bottom * (plevel_rel - 1) plevel_btm = top else: # linear scale bottom = max(self.zmin, H_min) # min. display value top = self.zmax # max. display value top_bottom = top - bottom # top = zmax_rel * H_max # calculate display top from max. of H(f) zlevel = bottom + top_bottom * zlevel_rel # height of displayed zero position if self.cmbMode3D.currentText() == 'None': # "Poleposition" for H(f) plot only #H_max = np.clip(max(H_abs), 0, self.zmax) # make height of displayed poles same to zeros plevel_top = bottom + top_bottom * zlevel_rel plevel_btm = bottom else: plevel_top = plevel_rel * top plevel_btm = top # calculate H(jw)| along the unity circle and |H(z)|, each clipped # between bottom and top H_UC = H_mag(bb, aa, self.xy_UC, top, H_min=bottom, log=self.chkLog.isChecked()) Hmag = H_mag(bb, aa, self.z, top, H_min=bottom, log=self.chkLog.isChecked()) #=============================================================== ## plot Unit Circle (UC) #=============================================================== if self.chkUC.isChecked(): # Plot unit circle and marker at (1,0): self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, ones(len(self.xy_UC)) * bottom, lw=2, color='k') self.ax3d.plot([0.97, 1.03], [0, 0], [bottom, bottom], lw=2, color='k') #=============================================================== ## plot ||H(f)| along unit circle as 3D-lineplot #=============================================================== if self.chkHf.isChecked(): self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, H_UC, alpha = 0.5) # draw once more as dashed white line to improve visibility self.ax3d.plot(self.xy_UC.real, self.xy_UC.imag, H_UC, 'w--') if stride < 10: # plot thin vertical line every stride points on the UC for k in range(len(self.xy_UC[::stride])): self.ax3d.plot([self.xy_UC.real[::stride][k], self.xy_UC.real[::stride][k]], [self.xy_UC.imag[::stride][k], self.xy_UC.imag[::stride][k]], [np.ones(len(self.xy_UC[::stride]))[k]*bottom, H_UC[::stride][k]], linewidth=1, color=(0.5, 0.5, 0.5)) #=============================================================== ## plot Poles and Zeros #=============================================================== if self.chkPZ.isChecked(): PN_SIZE = 8 # size of P/N symbols # Plot zero markers at |H(z_i)| = zlevel with "stems": self.ax3d.plot(zz.real, zz.imag, ones(len(zz)) * zlevel, 'o', markersize=PN_SIZE, markeredgecolor='blue', markeredgewidth=2.0, markerfacecolor='none') for k in range(len(zz)): # plot zero "stems" self.ax3d.plot([zz[k].real, zz[k].real], [zz[k].imag, zz[k].imag], [bottom, zlevel], linewidth=1, color='b') # Plot the poles at |H(z_p)| = plevel with "stems": self.ax3d.plot(np.real(pp), np.imag(pp), plevel_top, 'x', markersize=PN_SIZE, markeredgewidth=2.0, markeredgecolor='red') for k in range(len(pp)): # plot pole "stems" self.ax3d.plot([pp[k].real, pp[k].real], [pp[k].imag, pp[k].imag], [plevel_btm, plevel_top], linewidth=1, color='r') #=============================================================== ## 3D-Plots of |H(z)| clipped between |H(z)| = top #=============================================================== m_cb = cm.ScalarMappable(cmap=cmap) # normalized proxy object that is mappable m_cb.set_array(Hmag) # for colorbar #--------------------------------------------------------------- ## 3D-mesh plot #--------------------------------------------------------------- if self.cmbMode3D.currentText() == 'Mesh': # fig_mlab = mlab.figure(fgcolor=(0., 0., 0.), bgcolor=(1, 1, 1)) # self.ax3d.set_zlim(0,2) self.ax3d.plot_wireframe(self.x, self.y, Hmag, rstride=5, cstride=stride, linewidth=1, color='gray') #--------------------------------------------------------------- ## 3D-surface plot #--------------------------------------------------------------- # http://stackoverflow.com/questions/28232879/phong-shading-for-shiny-python-3d-surface-plots elif self.cmbMode3D.currentText() == 'Surf': if MLAB: ## Mayavi surf = mlab.surf(self.x, self.y, H_mag, colormap='RdYlBu', warp_scale='auto') # Change the visualization parameters. surf.actor.property.interpolation = 'phong' surf.actor.property.specular = 0.1 surf.actor.property.specular_power = 5 # s = mlab.contour_surf(self.x, self.y, Hmag, contour_z=0) mlab.show() else: if self.chkLighting.isChecked(): ls = LightSource(azdeg=0, altdeg=65) # Create light source object rgb = ls.shade(Hmag, cmap=cmap) # Shade data, creating an rgb array cmap_surf = None else: rgb = None cmap_surf = cmap # s = self.ax3d.plot_surface(self.x, self.y, Hmag, # alpha=OPT_3D_ALPHA, rstride=1, cstride=1, cmap=cmap, # linewidth=0, antialiased=False, shade=True, facecolors = rgb) # s.set_edgecolor('gray') s = self.ax3d.plot_surface(self.x, self.y, Hmag, alpha=alpha, rstride=1, cstride=1, linewidth=0, antialiased=False, facecolors=rgb, cmap=cmap_surf, shade=True) s.set_edgecolor(None) #--------------------------------------------------------------- ## 3D-Contour plot #--------------------------------------------------------------- elif self.cmbMode3D.currentText() == 'Contour': s = self.ax3d.contourf3D(self.x, self.y, Hmag, NL, alpha=alpha, cmap=cmap) #--------------------------------------------------------------- ## 2D-Contour plot # TODO: 2D contour plots do not plot correctly together with 3D plots in # current matplotlib 1.4.3 -> disable them for now # TODO: zdir = x / y delivers unexpected results -> rather plot max(H) # along the other axis? # TODO: colormap is created depending on the zdir = 'z' contour plot # -> set limits of (all) other plots manually? if self.chkContour2D.isChecked(): # self.ax3d.contourf(x, y, Hmag, 20, zdir='x', offset=xmin, # cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom) # self.ax3d.contourf(x, y, Hmag, 20, zdir='y', offset=ymax, # cmap=cmap, alpha = alpha)#, vmin = bottom)#, vmax = top, vmin = bottom) s = self.ax3d.contourf(self.x, self.y, Hmag, NL, zdir='z', offset=bottom - (top - bottom) * 0.05, cmap=cmap, alpha=alpha) # plot colorbar for suitable plot modes if self.chkColBar.isChecked() and (self.chkContour2D.isChecked() or str(self.cmbMode3D.currentText()) in {'Contour', 'Surf'}): self.colb = self.mplwidget.fig.colorbar(m_cb, ax=self.ax3d, shrink=0.8, aspect=20, pad=0.02, fraction=0.08) #---------------------------------------------------------------------- ## Set view limits and labels #---------------------------------------------------------------------- if not self.mplwidget.mplToolbar.a_lk.isChecked(): self.ax3d.set_xlim3d(self.xmin, self.xmax) self.ax3d.set_ylim3d(self.ymin, self.ymax) self.ax3d.set_zlim3d(bottom, top) else: self._restore_axes() self.ax3d.set_xlabel('Re')#(fb.fil[0]['plt_fLabel']) self.ax3d.set_ylabel('Im') #(r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $') # self.ax3d.set_zlabel(r'$|H(z)|\; \rightarrow $') self.ax3d.set_title(r'3D-Plot of $|H(\mathrm{e}^{\mathrm{j} \Omega})|$ and $|H(z)|$') self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class Plot_Hf(QWidget): """ Widget for plotting \|H(f)\|, frequency specs and the phase """ # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self, parent): super(Plot_Hf, self).__init__(parent) self.data_changed = True # flag whether plot needs to be updated self.ui_changed = True # flag whether plot needs to be updated self.needs_redraw = True # flag whether plot needs to be redrawn self.tool_tip = "Magnitude and phase frequency response" self.tab_label = "|H(f)|" self._construct_ui() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.data_changed, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'specs_changed' in dict_sig\ or 'home' in dict_sig or self.data_changed: self.draw() self.data_changed = False self.view_changed = False self.ui_changed = False if 'view_changed' in dict_sig or self.view_changed: self.update_view() self.view_changed = False self.ui_changed = False if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.ui_changed: self.redraw() self.ui_changed = False else: if 'data_changed' in dict_sig or 'specs_changed' in dict_sig: self.data_changed = True if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.ui_changed = True if 'view_changed' in dict_sig: self.view_changed = True def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip("Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel("in", self) units = ['dB', 'V', 'W', 'Auto'] self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip("<span>Set unit for y-axis:\n" "dB is attenuation (positive values), V and W are gain (less than 1).</span>") self.cmbUnitsA.setCurrentIndex(0) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkLinphase = QCheckBox("Zero phase", self) self.chkLinphase.setToolTip("<span>Subtract linear phase according to filter order.\n" "Attention: this makes no sense for a non-linear phase system!</span>") self.lblInset = QLabel("Inset", self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.chkSpecs = QCheckBox("Show Specs", self) self.chkSpecs.setChecked(False) self.chkSpecs.setToolTip("Display filter specs as hatched regions") self.chkPhase = QCheckBox("Phase", self) self.chkPhase.setToolTip("Overlay phase") #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmbUnitsA) layHControls.addStretch(1) layHControls.addWidget(self.chkLinphase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.chkSpecs) layHControls.addStretch(1) layHControls.addWidget(self.chkPhase) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self.draw) self.cmbShowH.currentIndexChanged.connect(self.draw) self.chkLinphase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.chkSpecs.clicked.connect(self.draw) self.chkPhase.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes (this is run only once) """ if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes self.ax = self.mplwidget.fig.add_subplot(111) self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def plot_spec_limits(self, ax): """ Plot the specifications limits (F_SB, A_SB, ...) as hatched areas with borders. """ hatch = params['mpl_hatch'] hatch_borders = params['mpl_hatch_border'] def dB(lin): return 20 * np.log10(lin) def _plot_specs(): # upper limits: ax.plot(F_lim_upl, A_lim_upl, F_lim_upc, A_lim_upc, F_lim_upr, A_lim_upr, **hatch_borders) if A_lim_upl: ax.fill_between(F_lim_upl, max(A_lim_upl), A_lim_upl, **hatch) if A_lim_upc: ax.fill_between(F_lim_upc, max(A_lim_upc), A_lim_upc, **hatch) if A_lim_upr: ax.fill_between(F_lim_upr, max(A_lim_upr), A_lim_upr, **hatch) # lower limits: ax.plot(F_lim_lol, A_lim_lol, F_lim_loc, A_lim_loc, F_lim_lor, A_lim_lor, **hatch_borders) if A_lim_lol: ax.fill_between(F_lim_lol, min(A_lim_lol), A_lim_lol, **hatch) if A_lim_loc: ax.fill_between(F_lim_loc, min(A_lim_loc), A_lim_loc, **hatch) if A_lim_lor: ax.fill_between(F_lim_lor, min(A_lim_lor), A_lim_lor, **hatch) if self.unitA == 'V': exp = 1. elif self.unitA == 'W': exp = 2. if self.unitA == 'dB': if fb.fil[0]['ft'] == "FIR": A_PB_max = dB(1 + self.A_PB) A_PB2_max = dB(1 + self.A_PB2) else: # IIR dB A_PB_max = A_PB2_max = 0 A_PB_min = dB(1 - self.A_PB) A_PB2_min = dB(1 - self.A_PB2) A_PB_minx = min(A_PB_min, A_PB2_min) - 5 A_PB_maxx = max(A_PB_max, A_PB2_max) + 5 A_SB = dB(self.A_SB) A_SB2 = dB(self.A_SB2) A_SB_maxx = max(A_SB, A_SB2) + 10 else: # 'V' or 'W' if fb.fil[0]['ft'] == "FIR": A_PB_max = (1 + self.A_PB)**exp A_PB2_max = (1 + self.A_PB2)**exp else: # IIR lin A_PB_max = A_PB2_max = 1 A_PB_min = (1 - self.A_PB)**exp A_PB2_min = (1 - self.A_PB2)**exp A_PB_minx = min(A_PB_min, A_PB2_min) / 1.05 A_PB_maxx = max(A_PB_max, A_PB2_max) * 1.05 A_SB = self.A_SB ** exp A_SB2 = self.A_SB2 ** exp A_SB_maxx = A_PB_min / 10. F_max = self.f_S/2 F_PB = self.F_PB F_SB = fb.fil[0]['F_SB'] * self.f_S F_SB2 = fb.fil[0]['F_SB2'] * self.f_S F_PB2 = fb.fil[0]['F_PB2'] * self.f_S F_lim_upl = F_lim_lol = [] # left side limits, lower and upper A_lim_upl = A_lim_lol = [] F_lim_upc = F_lim_loc = [] # center limits, lower and upper A_lim_upc = A_lim_loc = [] F_lim_upr = F_lim_lor = [] # right side limits, lower and upper A_lim_upr = A_lim_lor = [] if fb.fil[0]['rt'] == 'LP': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB, F_SB, F_max] A_lim_upr = [A_SB_maxx, A_SB, A_SB] if fb.fil[0]['rt'] == 'HP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB, F_PB, F_max] A_lim_upr = [A_PB_maxx, A_PB_max, A_PB_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB_min, A_PB_min] if fb.fil[0]['rt'] == 'BS': F_lim_upl = [0, F_PB, F_PB] A_lim_upl = [A_PB_max, A_PB_max, A_PB_maxx] F_lim_lol = F_lim_upl A_lim_lol = [A_PB_min, A_PB_min, A_PB_minx] F_lim_upc = [F_SB, F_SB, F_SB2, F_SB2] A_lim_upc = [A_SB_maxx, A_SB, A_SB, A_SB_maxx] F_lim_upr = [F_PB2, F_PB2, F_max] A_lim_upr = [A_PB_maxx, A_PB2_max, A_PB2_max] F_lim_lor = F_lim_upr A_lim_lor = [A_PB_minx, A_PB2_min, A_PB2_min] if fb.fil[0]['rt'] == 'BP': F_lim_upl = [0, F_SB, F_SB] A_lim_upl = [A_SB, A_SB, A_SB_maxx] F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = [F_SB2, F_SB2, F_max] A_lim_upr = [A_SB_maxx, A_SB2, A_SB2] if fb.fil[0]['rt'] == 'HIL': F_lim_upc = [F_PB, F_PB, F_PB2, F_PB2] A_lim_upc = [A_PB_maxx, A_PB_max, A_PB_max, A_PB_maxx] F_lim_loc = F_lim_upc A_lim_loc = [A_PB_minx, A_PB_min, A_PB_min, A_PB_minx] F_lim_upr = np.array(F_lim_upr) F_lim_lor = np.array(F_lim_lor) F_lim_upl = np.array(F_lim_upl) F_lim_lol = np.array(F_lim_lol) F_lim_upc = np.array(F_lim_upc) F_lim_loc = np.array(F_lim_loc) _plot_specs() # plot specs in the range 0 ... f_S/2 if fb.fil[0]['freqSpecsRangeType'] != 'half': # add plot limits for other half of the spectrum if fb.fil[0]['freqSpecsRangeType'] == 'sym': # frequency axis +/- f_S/2 F_lim_upl = -F_lim_upl F_lim_lol = -F_lim_lol F_lim_upc = -F_lim_upc F_lim_loc = -F_lim_loc F_lim_upr = -F_lim_upr F_lim_lor = -F_lim_lor else: # -> 'whole' F_lim_upl = self.f_S - F_lim_upl F_lim_lol = self.f_S - F_lim_lol F_lim_upc = self.f_S - F_lim_upc F_lim_loc = self.f_S - F_lim_loc F_lim_upr = self.f_S - F_lim_upr F_lim_lor = self.f_S - F_lim_lor _plot_specs() #------------------------------------------------------------------------------ def draw_inset(self): """ Construct / destruct second axes for an inset second plot """ # TODO: try ax1 = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6 # TODO: choose size & position of inset, maybe dependent on filter type # or specs (i.e. where is passband etc.) # DEBUG # print(self.cmbInset.currentIndex(), self.mplwidget.fig.axes) # list of axes in Figure # for ax in self.mplwidget.fig.axes: # print(ax) # print("cmbInset, inset_idx:",self.cmbInset.currentIndex(), self.inset_idx) if self.cmbInset.currentIndex() > 0: if self.inset_idx == 0: # Inset was turned off before, create a new one # Add an axes at position rect [left, bottom, width, height]: self.ax_i = self.mplwidget.fig.add_axes([0.65, 0.61, .3, .3]) self.ax_i.clear() # clear old plot and specs # draw an opaque background with the extent of the inset plot: # self.ax_i.patch.set_facecolor('green') # without label area # self.mplwidget.fig.patch.set_facecolor('green') # whole figure extent = self.mplwidget.get_full_extent(self.ax_i, pad = 0.0) # Transform this back to figure coordinates - otherwise, it # won't behave correctly when the size of the plot is changed: extent = extent.transformed(self.mplwidget.fig.transFigure.inverted()) rect = Rectangle((extent.xmin, extent.ymin), extent.width, extent.height, facecolor=rcParams['figure.facecolor'], edgecolor='none', transform=self.mplwidget.fig.transFigure, zorder=-1) self.ax_i.patches.append(rect) self.ax_i.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_i.plot(self.F, self.H_plt) if self.cmbInset.currentIndex() == 1: # edit / navigate inset self.ax_i.set_navigate(True) self.ax.set_navigate(False) if self.specs: self.plot_spec_limits(self.ax_i) else: # edit / navigate main plot self.ax_i.set_navigate(False) self.ax.set_navigate(True) else: # inset has been turned off, delete it self.ax.set_navigate(True) try: #remove ax_i from the figure self.mplwidget.fig.delaxes(self.ax_i) except AttributeError: pass self.inset_idx = self.cmbInset.currentIndex() # update index self.draw() #------------------------------------------------------------------------------ def draw_phase(self, ax): """ Draw phase on second y-axis in the axes system passed as the argument """ try: self.mplwidget.fig.delaxes(self.ax_p) except (KeyError, AttributeError): pass if self.chkPhase.isChecked(): self.ax_p = ax.twinx() # second axes system with same x-axis for phase self.ax_p.is_twin = True # mark this as 'twin' to suppress second grid in mpl_widget # phi_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$' if fb.fil[0]['plt_phiUnit'] == 'rad': phi_str += ' in rad ' + r'$\rightarrow $' scale = 1. elif fb.fil[0]['plt_phiUnit'] == 'rad/pi': phi_str += ' in rad' + r'$ / \pi \;\rightarrow $' scale = 1./ np.pi else: phi_str += ' in deg ' + r'$\rightarrow $' scale = 180./np.pi # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans phi = np.angle(np.nan_to_num(self.H_c)) #----------------------------------------------------------- self.ax_p.plot(self.F,np.unwrap(phi)*scale, 'g-.', label = "Phase") #----------------------------------------------------------- self.ax_p.set_ylabel(phi_str) nbins = len(self.ax.get_yticks()) # number of ticks on main y-axis # http://stackoverflow.com/questions/28692608/align-grid-lines-on-two-plots # http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # http://stackoverflow.com/questions/20243683/matplotlib-align-twinx-tick-marks # manual setting: #self.ax_p.set_yticks( np.linspace(self.ax_p.get_ylim()[0],self.ax_p.get_ylim()[1],nbins) ) #ax1.set_yticks(np.linspace(ax1.get_ybound()[0], ax1.get_ybound()[1], 5)) #ax2.set_yticks(np.linspace(ax2.get_ybound()[0], ax2.get_ybound()[1], 5)) #http://stackoverflow.com/questions/3654619/matplotlib-multiple-y-axes-grid-lines-applied-to-both # use helper functions from matplotlib.ticker: # MaxNLocator: set no more than nbins + 1 ticks #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.MaxNLocator(nbins = nbins) ) # further options: integer = False, # prune = [‘lower’ | ‘upper’ | ‘both’ | None] Remove edge ticks # AutoLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.AutoLocator() ) # LinearLocator: #self.ax_p.yaxis.set_major_locator( matplotlib.ticker.LinearLocator(numticks = nbins -1 ) ) # self.ax_p.locator_params(axis = 'y', nbins = nbins) # # self.ax_p.set_yticks(np.linspace(self.ax_p.get_ybound()[0], # self.ax_p.get_ybound()[1], # len(self.ax.get_yticks())-1)) #N = source_ax.xaxis.get_major_ticks() #target_ax.xaxis.set_major_locator(LinearLocator(N)) # else: # try: # self.mplwidget.fig.delaxes(self.ax_p) # except (KeyError, AttributeError): # pass # self.draw() #------------------------------------------------------------------------------ def calc_hf(self): """ (Re-)Calculate the complex frequency response H(f) """ # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], True) #------------------------------------------------------------------------------ def draw(self): """ Re-calculate \|H(f)\| and draw the figure """ self.calc_hf() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ # Get corners for spec display from the parameters of the target specs subwidget try: param_list = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']]\ [fb.fil[0]['fc']][fb.fil[0]['fo']]['tspecs'][1]['amp'] except KeyError: param_list = [] SB = [l for l in param_list if 'A_SB' in l] PB = [l for l in param_list if 'A_PB' in l] if SB: A_min = min([fb.fil[0][l] for l in SB]) else: A_min = 5e-4 if PB: A_max = max([fb.fil[0][l] for l in PB]) else: A_max = 1 if np.all(self.W) is None: # H(f) has not been calculated yet self.calc_hf() if self.cmbUnitsA.currentText() == 'Auto': self.unitA = fb.fil[0]['amp_specs_unit'] else: self.unitA = self.cmbUnitsA.currentText() # Linphase settings only makes sense for amplitude plot self.chkLinphase.setCheckable(self.unitA == 'V') self.chkLinphase.setEnabled(self.unitA == 'V') self.specs = self.chkSpecs.isChecked() self.linphase = self.chkLinphase.isChecked() self.f_S = fb.fil[0]['f_S'] self.F_PB = fb.fil[0]['F_PB'] * self.f_S self.F_SB = fb.fil[0]['F_SB'] * self.f_S self.A_PB = fb.fil[0]['A_PB'] self.A_PB2 = fb.fil[0]['A_PB2'] self.A_SB = fb.fil[0]['A_SB'] self.A_SB2 = fb.fil[0]['A_SB2'] f_lim = fb.fil[0]['freqSpecsRange'] #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c self.F = self.W / (2 * np.pi) * self.f_S if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 self.H_c = np.fft.fftshift(self.H_cmplx) self.F -= self.f_S/2. elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F self.H_c = self.H_cmplx[0:params['N_FFT']//2] self.F = self.F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated self.H_c = self.H_cmplx # now calculate mag / real / imaginary part of H_c: if self.linphase: # remove the linear phase self.H_c = self.H_c * np.exp(1j * self.W[0:len(self.F)] * fb.fil[0]["N"]/2.) if self.cmbShowH.currentIndex() == 0: # show magnitude of H H = abs(self.H_c) H_str = r'$|H(\mathrm{e}^{\mathrm{j} \Omega})|$' elif self.cmbShowH.currentIndex() == 1: # show real part of H H = self.H_c.real H_str = r'$\Re \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' else: # show imag. part of H H = self.H_c.imag H_str = r'$\Im \{H(\mathrm{e}^{\mathrm{j} \Omega})\}$' #================ Main Plotting Routine ========================= #=== clear the axes and (re)draw the plot (if selectable) if self.ax.get_navigate(): if self.unitA == 'dB': A_lim = [20*np.log10(A_min) -10, 20*np.log10(1+A_max) +1] self.H_plt = 20*np.log10(abs(H)) H_str += ' in dB ' + r'$\rightarrow$' elif self.unitA == 'V': # 'lin' A_lim = [0, (1.05 + A_max)] self.H_plt = H H_str +=' in V ' + r'$\rightarrow $' self.ax.axhline(linewidth=1, color='k') # horizontal line at 0 else: # unit is W A_lim = [0, (1.03 + A_max)**2.] self.H_plt = H * H.conj() H_str += ' in W ' + r'$\rightarrow $' #----------------------------------------------------------- self.ax.clear() self.ax.plot(self.F, self.H_plt, label = 'H(f)') # TODO: self.draw_inset() # this gives an infinite recursion self.draw_phase(self.ax) #----------------------------------------------------------- #============= Set Limits and draw specs ========================= if self.specs: self.plot_spec_limits(self.ax) # self.ax_bounds = [self.ax.get_ybound()[0], self.ax.get_ybound()[1]]#, self.ax.get] self.ax.set_xlim(f_lim) self.ax.set_ylim(A_lim) self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(H_str) self.ax.set_title(r'Magnitude Frequency Response') self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class Plot_Phi(QWidget): # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) def __init__(self, parent): super(Plot_Phi, self).__init__(parent) self.needs_calc = True # recalculation of filter function necessary self.needs_draw = True # plotting neccessary (e.g. log instead of lin) self.needs_redraw = True # redraw (e.g. because size has changed) neccessary self.tool_tip = "Phase frequency response" self.tab_label = "\u03C6(f)" # phi(f) self._construct_UI() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_calc = {1}, visible = {2}"\ .format(dict_sig, self.needs_calc, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False self.needs_draw = False elif 'view_changed' in dict_sig or self.needs_draw: self.update_view() self.needs_draw = False elif ('ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized')\ or self.needs_redraw: self.redraw() else: if 'data_changed' in dict_sig: self.needs_calc = True elif 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig[ 'ui_changed'] == 'resized': self.needs_redraw = True def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.cmbUnitsPhi = QComboBox(self) units = ["rad", "rad/pi", "deg"] scales = [1., 1. / np.pi, 180. / np.pi] for unit, scale in zip(units, scales): self.cmbUnitsPhi.addItem(unit, scale) self.cmbUnitsPhi.setObjectName("cmbUnitsA") self.cmbUnitsPhi.setToolTip("Set unit for phase.") self.cmbUnitsPhi.setCurrentIndex(0) self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkWrap = QCheckBox("Wrapped Phase", self) self.chkWrap.setChecked(False) self.chkWrap.setToolTip("Plot phase wrapped to +/- pi") layHControls = QHBoxLayout() layHControls.addWidget(self.cmbUnitsPhi) layHControls.addWidget(self.chkWrap) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.chkWrap.clicked.connect(self.draw) self.cmbUnitsPhi.currentIndexChanged.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes - this is only called once """ if len(self.mplwidget.fig.get_axes()) == 0: # empty figure, no axes #self.ax = self.mplwidget.fig.add_subplot(111) self.ax = self.mplwidget.fig.subplots() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def calc_resp(self): """ (Re-)Calculate the complex frequency response H(f) """ # calculate H_cplx(W) (complex) for W = 0 ... 2 pi: self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], wholeF=True) # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans self.H_cmplx = np.nan_to_num(self.H_cmplx) #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate \|H(f)\| and draw the figure """ self.calc_resp() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ self.unitPhi = qget_cmb_box(self.cmbUnitsPhi, data=False) f_S2 = fb.fil[0]['f_S'] / 2. #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c F = self.W * f_S2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 H = np.fft.fftshift(self.H_cmplx) F -= f_S2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F H = self.H_cmplx[0:params['N_FFT'] // 2] F = F[0:params['N_FFT'] // 2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated H = self.H_cmplx y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in ' if self.unitPhi == 'rad': y_str += 'rad ' + r'$\rightarrow $' scale = 1. elif self.unitPhi == 'rad/pi': y_str += 'rad' + r'$ / \pi \;\rightarrow $' scale = 1. / np.pi else: y_str += 'deg ' + r'$\rightarrow $' scale = 180. / np.pi fb.fil[0]['plt_phiLabel'] = y_str fb.fil[0]['plt_phiUnit'] = self.unitPhi if self.chkWrap.isChecked(): phi_plt = np.angle(H) * scale else: phi_plt = np.unwrap(np.angle(H)) * scale #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_phi, = self.ax.plot(F, phi_plt) #--------------------------------------------------------- self.ax.set_title(r'Phase Frequency Response') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(y_str) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
def _construct_ui(self): """ Define and construct the subwidgets """ modes = ['| H |', 're{H}', 'im{H}'] self.cmbShowH = QComboBox(self) self.cmbShowH.addItems(modes) self.cmbShowH.setObjectName("cmbUnitsH") self.cmbShowH.setToolTip("Show magnitude, real / imag. part of H or H \n" "without linear phase (acausal system).") self.cmbShowH.setCurrentIndex(0) self.lblIn = QLabel("in", self) units = ['dB', 'V', 'W', 'Auto'] self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip("<span>Set unit for y-axis:\n" "dB is attenuation (positive values), V and W are gain (less than 1).</span>") self.cmbUnitsA.setCurrentIndex(0) self.cmbShowH.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkLinphase = QCheckBox("Zero phase", self) self.chkLinphase.setToolTip("<span>Subtract linear phase according to filter order.\n" "Attention: this makes no sense for a non-linear phase system!</span>") self.lblInset = QLabel("Inset", self) self.cmbInset = QComboBox(self) self.cmbInset.addItems(['off', 'edit', 'fixed']) self.cmbInset.setObjectName("cmbInset") self.cmbInset.setToolTip("Display/edit second inset plot") self.cmbInset.setCurrentIndex(0) self.inset_idx = 0 # store previous index for comparison self.chkSpecs = QCheckBox("Show Specs", self) self.chkSpecs.setChecked(False) self.chkSpecs.setToolTip("Display filter specs as hatched regions") self.chkPhase = QCheckBox("Phase", self) self.chkPhase.setToolTip("Overlay phase") #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.cmbShowH) layHControls.addWidget(self.lblIn) layHControls.addWidget(self.cmbUnitsA) layHControls.addStretch(1) layHControls.addWidget(self.chkLinphase) layHControls.addStretch(1) layHControls.addWidget(self.lblInset) layHControls.addWidget(self.cmbInset) layHControls.addStretch(1) layHControls.addWidget(self.chkSpecs) layHControls.addStretch(1) layHControls.addWidget(self.chkPhase) layHControls.addStretch(10) self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # calculate and draw |H(f)| #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self.draw) self.cmbShowH.currentIndexChanged.connect(self.draw) self.chkLinphase.clicked.connect(self.draw) self.cmbInset.currentIndexChanged.connect(self.draw_inset) self.chkSpecs.clicked.connect(self.draw) self.chkPhase.clicked.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx)
class Plot_tau_g(QWidget): """ Widget for plotting the group delay """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self): super().__init__() self.verbose = False # suppress warnings self.algorithm = "auto" self.needs_calc = True # flag whether plot needs to be recalculated self.tool_tip = self.tr("Group delay") self.tab_label = "\U0001D70F(f)" # "tau_g" \u03C4 self.cmb_algorithm_items =\ ["<span>Select algorithm for calculating the group delay.</span>", ("auto", "Auto", "<span>Try to find best-suited algorithm.</span>"), ("scipy", "Scipy", "<span>Scipy algorithm.</span>"), ("jos", "JOS", "<span>J.O. Smith's algorithm.</span>"), ("shpak", "Shpak", "<span>Shpak's algorithm for SOS and other IIR" "filters.</span>"), ("diff", "Diff", "<span>Textbook-style, differentiate the phase." "</span>") ] self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkWarnings = QCheckBox(self.tr("Verbose"), self) self.chkWarnings.setChecked(self.verbose) self.chkWarnings.setToolTip( self. tr("<span>Print messages about singular group delay and calculation times." "</span>")) self.cmbAlgorithm = QComboBox(self) qcmb_box_populate(self.cmbAlgorithm, self.cmb_algorithm_items, self.algorithm) layHControls = QHBoxLayout() layHControls.addStretch(10) layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) layHControls.addWidget(self.cmbAlgorithm) # This widget encompasses all control subwidgets: self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['mpl_margins']) self.mplwidget.mplToolbar.a_he.setEnabled(True) self.mplwidget.mplToolbar.a_he.info = "manual/plot_tau_g.html" self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_sig_rx) self.cmbAlgorithm.currentIndexChanged.connect(self.draw) # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ # logger.debug("Processing {0} | needs_calc = {1}, visible = {2}" # .format(dict_sig, self.needs_calc, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_calc: self.draw() self.needs_calc = False elif 'view_changed' in dict_sig: self.update_view() else: if 'data_changed' in dict_sig or 'view_changed' in dict_sig: self.needs_calc = True # ------------------------------------------------------------------------------ def init_axes(self): """ Initialize the axes and set some stuff that is not cleared by `ax.clear()` later on. """ self.ax = self.mplwidget.fig.subplots() self.ax.xaxis.tick_bottom() # remove axis ticks on top self.ax.yaxis.tick_left() # remove axis ticks right # ------------------------------------------------------------------------------ def calc_tau_g(self): """ (Re-)Calculate the complex frequency response H(f) """ bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: # scipy: self.W, self.tau_g = group_delay((bb, aa), w=params['N_FFT'], # whole = True) if fb.fil[0]['creator'][0] == 'sos': # one of 'sos', 'zpk', 'ba' self.W, self.tau_g = group_delay( fb.fil[0]['sos'], nfft=params['N_FFT'], sos=True, whole=True, verbose=self.chkWarnings.isChecked(), alg=self.cmbAlgorithm.currentData()) else: self.W, self.tau_g = group_delay( bb, aa, nfft=params['N_FFT'], whole=True, verbose=self.chkWarnings.isChecked(), alg=self.cmbAlgorithm.currentData()) # self.chkWarnings.isChecked()) # Zero phase filters have no group delay (Causal+AntiCausal) if 'baA' in fb.fil[0]: self.tau_g = np.zeros(self.tau_g.size) # ------------------------------------------------------------------------------ def draw(self): self.calc_tau_g() self.update_view() # ------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ # ========= select frequency range to be displayed ===================== # === shift, scale and select: W -> F, H_cplx -> H_c f_max_2 = fb.fil[0]['f_max'] / 2. F = self.W * f_max_2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift tau_g and F by f_S/2 tau_g = np.fft.fftshift(self.tau_g) F -= f_max_2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F tau_g = self.tau_g[0:params['N_FFT'] // 2] F = F[0:params['N_FFT'] // 2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated tau_g = self.tau_g # ================ Main Plotting Routine ========================= # === clear the axes and (re)draw the plot if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $' else: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\ + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $' tau_g = tau_g / fb.fil[0]['f_S'] # --------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_tau_g, = self.ax.plot(F, tau_g, label="Group Delay") # --------------------------------------------------------- self.ax.xaxis.set_minor_locator( AutoMinorLocator()) # enable minor ticks self.ax.yaxis.set_minor_locator( AutoMinorLocator()) # enable minor ticks self.ax.set_title(r'Group Delay $ \tau_g$') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(tau_str) # widen y-limits to suppress numerical inaccuracies when tau_g = constant self.ax.set_ylim( [max(np.nanmin(tau_g) - 0.5, 0), np.nanmax(tau_g) + 0.5]) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() # ------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class PlotPZ(QWidget): def __init__(self, parent): super(PlotPZ, self).__init__(parent) self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.chkHf = QCheckBox("Show |H(f)|", self) self.chkHf.setToolTip("<span>Enable display of |H(f)|.</span>") self.chkHf.setEnabled(True) layHControls = QHBoxLayout() layHControls.addWidget(self.chkHf) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) # make this the central widget, taking all available space: # self.setCentralWidget(self.mplwidget) self.init_axes() self.draw() # calculate and draw poles and zeros #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) self.chkHf.clicked.connect(self.draw) #------------------------------------------------------------------------------ @pyqtSlot(object) def process_signals(self, sig_dict): """ Process signals coming from the navigation toolbar """ if 'update_view' in sig_dict: self.update_view() elif 'enabled' in sig_dict: self.enable_ui(sig_dict['enabled']) elif 'home' in sig_dict: self.draw() else: pass #------------------------------------------------------------------------------ def enable_ui(self, enabled): """ Triggered when the toolbar is enabled or disabled """ # self.frmControls.setEnabled(enabled) # no control widgets yet if enabled: self.draw() #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes """ if self.chkHf.isChecked(): self.ax = self.mplwidget.fig.add_subplot(111) else: self.ax = self.mplwidget.fig.add_subplot(111) self.ax.clear() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etcs without recalculating H(f) -- not yet implemented, just use draw() for the moment """ self.draw() #------------------------------------------------------------------------------ def draw(self): if self.mplwidget.mplToolbar.enabled: self.init_axes() self.draw_pz() #------------------------------------------------------------------------------ def draw_pz(self): """ (re)draw P/Z plot """ p_marker = params['P_Marker'] z_marker = params['Z_Marker'] zpk = fb.fil[0]['zpk'] # add antiCausals if they exist (must take reciprocal to plot) if 'rpk' in fb.fil[0]: zA = fb.fil[0]['zpk'][0] zA = np.conj(1./zA) pA = fb.fil[0]['zpk'][1] pA = np.conj(1./pA) zC = np.append(zpk[0],zA) pC = np.append(zpk[1],pA) zpk[0] = zC zpk[1] = pC self.ax.clear() [z,p,k] = self.zplane(z = zpk[0], p = zpk[1], k = zpk[2], plt_ax = self.ax, mps = p_marker[0], mpc = p_marker[1], mzs = z_marker[0], mzc = z_marker[1]) self.ax.set_title(r'Pole / Zero Plot') self.ax.set_xlabel('Real axis') self.ax.set_ylabel('Imaginary axis') if self.chkHf.isChecked(): self.draw_Hf() self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw() #------------------------------------------------------------------------------ def zplane(self, b=None, a=1, z=None, p=None, k =1, pn_eps=1e-3, analog=False, plt_ax = None, style='square', anaCircleRad=0, lw=2, mps = 10, mzs = 10, mpc = 'r', mzc = 'b', plabel = '', zlabel = ''): """ Plot the poles and zeros in the complex z-plane either from the coefficients (`b,`a) of a discrete transfer function `H`(`z`) (zpk = False) or directly from the zeros and poles (z,p) (zpk = True). When only b is given, an FIR filter with all poles at the origin is assumed. Parameters ---------- b : array_like Numerator coefficients (transversal part of filter) When b is not None, poles and zeros are determined from the coefficients b and a a : array_like (optional, default = 1 for FIR-filter) Denominator coefficients (recursive part of filter) z : array_like, default = None Zeros When b is None, poles and zeros are taken directly from z and p p : array_like, default = None Poles analog : boolean (default: False) When True, create a P/Z plot suitable for the s-plane, i.e. suppress the unit circle (unless anaCircleRad > 0) and scale the plot for a good display of all poles and zeros. pn_eps : float (default : 1e-2) Tolerance for separating close poles or zeros plt_ax : handle to axes for plotting (default: None) When no axes is specified, the current axes is determined via plt.gca() pltLib : string (default: 'matplotlib') Library for plotting the P/Z plane. Currently, only matplotlib is implemented. When pltLib = 'none' or when matplotlib is not available, only pass the poles / zeros and their multiplicity style : string (default: 'square') Style of the plot, for style == 'square' make scale of x- and y- axis equal. mps : integer (default: 10) Size for pole marker mzs : integer (default: 10) Size for zero marker mpc : char (default: 'r') Pole marker colour mzc : char (default: 'b') Zero marker colour lw : integer (default: 2) Linewidth for unit circle plabel, zlabel : string (default: '') This string is passed to the plot command for poles and zeros and can be displayed by legend() Returns ------- z, p, k : ndarray Notes ----- """ # TODO: # - polar option # - add keywords for color of circle -> **kwargs # - add option for multi-dimensional arrays and zpk data # make sure that all inputs are arrays b = np.atleast_1d(b) a = np.atleast_1d(a) z = np.atleast_1d(z) # make sure that p, z are arrays p = np.atleast_1d(p) if b.any(): # coefficients were specified if len(b) < 2 and len(a) < 2: logger.error('No proper filter coefficients: both b and a are scalars!') return z, p, k # The coefficients are less than 1, normalize the coefficients if np.max(b) > 1: kn = np.max(b) b = b / float(kn) else: kn = 1. if np.max(a) > 1: kd = np.max(a) a = a / abs(kd) else: kd = 1. # Calculate the poles, zeros and scaling factor p = np.roots(a) z = np.roots(b) k = kn/kd elif not (len(p) or len(z)): # P/Z were specified logger.error('Either b,a or z,p must be specified!') return z, p, k # find multiple poles and zeros and their multiplicities if len(p) < 2: # single pole, [None] or [0] if not p or p == 0: # only zeros, create equal number of poles at origin p = np.array(0,ndmin=1) # num_p = np.atleast_1d(len(z)) else: num_p = [1.] # single pole != 0 else: #p, num_p = sig.signaltools.unique_roots(p, tol = pn_eps, rtype='avg') p, num_p = unique_roots(p, tol = pn_eps, rtype='avg') # p = np.array(p); num_p = np.ones(len(p)) if len(z) > 0: z, num_z = unique_roots(z, tol = pn_eps, rtype='avg') # z = np.array(z); num_z = np.ones(len(z)) #z, num_z = sig.signaltools.unique_roots(z, tol = pn_eps, rtype='avg') else: num_z = [] ax = plt_ax#.subplot(111) if analog == False: # create the unit circle for the z-plane uc = patches.Circle((0,0), radius=1, fill=False, color='grey', ls='solid', zorder=1) ax.add_patch(uc) if style == 'square': r = 1.1 ax.axis([-r, r, -r, r], 'equal') ax.axis('equal') # ax.spines['left'].set_position('center') # ax.spines['bottom'].set_position('center') # ax.spines['right'].set_visible(True) # ax.spines['top'].set_visible(True) else: # s-plane if anaCircleRad > 0: # plot a circle with radius = anaCircleRad uc = patches.Circle((0,0), radius=anaCircleRad, fill=False, color='grey', ls='solid', zorder=1) ax.add_patch(uc) # plot real and imaginary axis ax.axhline(lw=2, color = 'k', zorder=1) ax.axvline(lw=2, color = 'k', zorder=1) # Plot the zeros ax.scatter(z.real, z.imag, s=mzs*mzs, zorder=2, marker = 'o', facecolor = 'none', edgecolor = mzc, lw = lw, label=zlabel) # Plot the poles ax.scatter(p.real, p.imag, s=mps*mps, zorder=2, marker='x', color=mpc, lw=lw, label=plabel) # Print multiplicity of poles / zeros for i in range(len(z)): logger.debug('z: {0} | {1} | {2}'.format(i, z[i], num_z[i])) if num_z[i] > 1: ax.text(np.real(z[i]), np.imag(z[i]),' (' + str(num_z[i]) +')', va = 'top', color=mzc) for i in range(len(p)): logger.debug('p:{0} | {1} | {2}'.format(i, p[i], num_p[i])) if num_p[i] > 1: ax.text(np.real(p[i]), np.imag(p[i]), ' (' + str(num_p[i]) +')', va = 'bottom', color=mpc) # increase distance between ticks and labels # to give some room for poles and zeros for tick in ax.get_xaxis().get_major_ticks(): tick.set_pad(12.) tick.label1 = tick._get_text1() for tick in ax.get_yaxis().get_major_ticks(): tick.set_pad(12.) tick.label1 = tick._get_text1() xl = ax.get_xlim(); Dx = max(abs(xl[1]-xl[0]), 0.05) yl = ax.get_ylim(); Dy = max(abs(yl[1]-yl[0]), 0.05) ax.set_xlim((xl[0]-Dx*0.05, max(xl[1]+Dx*0.05,0))) ax.set_ylim((yl[0]-Dy*0.05, yl[1] + Dy*0.05)) return z, p, k #------------------------------------------------------------------------------ def draw_Hf(self, r=2): """ Draw the magnitude frequency response around the UC """ ba = fb.fil[0]['ba'] w, h = sig.freqz(ba[0], ba[1], whole=True) h = np.abs(h) h = h / np.max(h) +1 # map |H(f)| to a range 1 ... 2 y = h * np.sin(w) x = h * np.cos(w) self.ax.plot(x,y, label="|H(f)|") uc = patches.Circle((0,0), radius=r, fill=False, color='grey', ls='dashed', zorder=1) self.ax.add_patch(uc) xl = self.ax.get_xlim() xmax = max(abs(xl[0]), abs(xl[1]), r*1.05) yl = self.ax.get_ylim() ymax = max(abs(yl[0]), abs(yl[1]), r*1.05) self.ax.set_xlim((-xmax, xmax)) self.ax.set_ylim((-ymax, ymax))
class PlotPhi(QWidget): def __init__(self, parent): super(PlotPhi, self).__init__(parent) self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements """ self.cmbUnitsPhi = QComboBox(self) units = ["rad", "rad/pi", "deg"] scales = [1., 1./ np.pi, 180./np.pi] for unit, scale in zip(units, scales): self.cmbUnitsPhi.addItem(unit, scale) self.cmbUnitsPhi.setObjectName("cmbUnitsA") self.cmbUnitsPhi.setToolTip("Set unit for phase.") self.cmbUnitsPhi.setCurrentIndex(0) self.cmbUnitsPhi.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.chkWrap = QCheckBox("Wrapped Phase", self) self.chkWrap.setChecked(False) self.chkWrap.setToolTip("Plot phase wrapped to +/- pi") layHControls = QHBoxLayout() # layHControls.addStretch(10) layHControls.addWidget(self.cmbUnitsPhi) layHControls.addWidget(self.chkWrap) layHControls.addStretch(10) #---------------------------------------------------------------------- # ### frmControls ### # # This widget encompasses all control subwidgets #---------------------------------------------------------------------- self.frmControls = QFrame(self) self.frmControls.setObjectName("frmControls") self.frmControls.setLayout(layHControls) #---------------------------------------------------------------------- # ### mplwidget ### # # main widget, encompassing the other widgets #---------------------------------------------------------------------- self.mplwidget = MplWidget(self) self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing # #============================================= # # Signals & Slots # #============================================= self.chkWrap.clicked.connect(self.draw) self.cmbUnitsPhi.currentIndexChanged.connect(self.draw) self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) #------------------------------------------------------------------------------ @pyqtSlot(object) def process_signals(self, sig_dict): """ Process signals coming from the navigation toolbar """ if 'update_view' in sig_dict: self.update_view() elif 'enabled' in sig_dict: self.enable_ui(sig_dict['enabled']) elif 'home' in sig_dict: self.draw() else: pass #------------------------------------------------------------------------------ def enable_ui(self, enabled): """ Triggered when the toolbar is enabled or disabled """ self.frmControls.setEnabled(enabled) if enabled: self.init_axes() self.draw() #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes """ # self.ax = self.mplwidget.ax self.ax = self.mplwidget.fig.add_subplot(111) self.ax.clear() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def calc_hf(self): """ (Re-)Calculate the complex frequency response H(f) """ # calculate H_cplx(W) (complex) for W = 0 ... 2 pi: self.W, self.H_cmplx = calc_Hcomplex(fb.fil[0], params['N_FFT'], wholeF=True) # replace nan and inf by finite values, otherwise np.unwrap yields # an array full of nans self.H_cmplx = np.nan_to_num(self.H_cmplx) #------------------------------------------------------------------------------ def draw(self): """ Main entry point: Re-calculate |H(f)| and draw the figure if enabled """ if self.mplwidget.mplToolbar.enabled: self.calc_hf() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ self.unitPhi = self.cmbUnitsPhi.currentText() f_S2 = fb.fil[0]['f_S'] / 2. #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c F = self.W * f_S2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift H and F by f_S/2 H = np.fft.fftshift(self.H_cmplx) F -= f_S2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F H = self.H_cmplx[0:params['N_FFT']//2] F = F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated H = self.H_cmplx y_str = r'$\angle H(\mathrm{e}^{\mathrm{j} \Omega})$ in ' if self.unitPhi == 'rad': y_str += 'rad ' + r'$\rightarrow $' scale = 1. elif self.unitPhi == 'rad/pi': y_str += 'rad' + r'$ / \pi \;\rightarrow $' scale = 1./ np.pi else: y_str += 'deg ' + r'$\rightarrow $' scale = 180./np.pi fb.fil[0]['plt_phiLabel'] = y_str fb.fil[0]['plt_phiUnit'] = self.unitPhi if self.chkWrap.isChecked(): phi_plt = np.angle(H) * scale else: phi_plt = np.unwrap(np.angle(H)) * scale #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_phi, = self.ax.plot(F, phi_plt) #--------------------------------------------------------- self.ax.set_title(r'Phase Frequency Response') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(y_str) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()
class Plot_Tau_G(QWidget): """ Widget for plotting the group delay """ # incoming, connected in sender widget (locally connected to self.process_signals() ) sig_rx = pyqtSignal(object) # sig_tx = pyqtSignal(object) # outgoing from process_signals def __init__(self, parent): super(Plot_Tau_G, self).__init__(parent) self.verbose = True # suppress warnings self.needs_draw = True # flag whether plot needs to be updated self.needs_redraw = True # flag whether plot needs to be redrawn self.tool_tip = "Group delay" self.tab_label = "tau_g" self._construct_UI() def _construct_UI(self): """ Intitialize the widget, consisting of: - Matplotlib widget with NavigationToolbar - Frame with control elements (currently commented out) """ # ============================================================================= # #### not needed at the moment ### # self.chkWarnings = QCheckBox("Enable Warnings", self) # self.chkWarnings.setChecked(False) # self.chkWarnings.setToolTip("Print warnings about singular group delay") # # self.chkScipy = QCheckBox("Scipy", self) # self.chkScipy.setChecked(False) # self.chkScipy.setToolTip("Use scipy group delay routine") # # layHControls = QHBoxLayout() # layHControls.addStretch(10) # layHControls.addWidget(self.chkWarnings) # layHControls.addWidget(self.chkScipy) # # # This widget encompasses all control subwidgets: # self.frmControls = QFrame(self) # self.frmControls.setObjectName("frmControls") # self.frmControls.setLayout(layHControls) # ============================================================================= self.mplwidget = MplWidget(self) #self.mplwidget.layVMainMpl.addWidget(self.frmControls) self.mplwidget.layVMainMpl.setContentsMargins(*params['wdg_margins']) self.setLayout(self.mplwidget.layVMainMpl) self.init_axes() self.draw() # initial drawing of tau_g #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_signals) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.mplwidget.mplToolbar.sig_tx.connect(self.process_signals) #------------------------------------------------------------------------------ def process_signals(self, dict_sig=None): """ Process signals coming from the navigation toolbar and from sig_rx """ logger.debug("Processing {0} | needs_draw = {1}, visible = {2}"\ .format(dict_sig, self.needs_draw, self.isVisible())) if self.isVisible(): if 'data_changed' in dict_sig or 'home' in dict_sig or self.needs_draw: self.draw() self.needs_draw = False self.needs_redraw = False elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized'\ or self.needs_redraw: self.redraw() self.needs_redraw = False elif 'view_changed' in dict_sig: self.update_view() else: if 'data_changed' in dict_sig or 'view_changed' in dict_sig: self.needs_draw = True elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'resized': self.needs_redraw = True #------------------------------------------------------------------------------ def init_axes(self): """ Initialize and clear the axes """ self.ax = self.mplwidget.fig.add_subplot(111) self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def calc_tau_g(self): """ (Re-)Calculate the complex frequency response H(f) """ bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] # calculate H_cmplx(W) (complex) for W = 0 ... 2 pi: self.W, self.tau_g = group_delay((bb, aa), w=params['N_FFT'], whole = True) #verbose = self.verbose) # self.chkWarnings.isChecked()) # Zero phase filters have no group delay (Causal+AntiCausal) if 'baA' in fb.fil[0]: self.tau_g = np.zeros(self.tau_g.size) #------------------------------------------------------------------------------ def draw(self): self.calc_tau_g() self.update_view() #------------------------------------------------------------------------------ def update_view(self): """ Draw the figure with new limits, scale etc without recalculating H(f) """ #========= select frequency range to be displayed ===================== #=== shift, scale and select: W -> F, H_cplx -> H_c f_S2 = fb.fil[0]['f_S'] / 2. F = self.W * f_S2 / np.pi if fb.fil[0]['freqSpecsRangeType'] == 'sym': # shift tau_g and F by f_S/2 tau_g = np.fft.fftshift(self.tau_g) F -= f_S2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # only use the first half of H and F tau_g = self.tau_g[0:params['N_FFT']//2] F = F[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # use H and F as calculated tau_g = self.tau_g #================ Main Plotting Routine ========================= #=== clear the axes and (re)draw the plot if fb.fil[0]['freq_specs_unit'] in {'f_S', 'f_Ny'}: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega}) / T_S \; \rightarrow $' else: tau_str = r'$ \tau_g(\mathrm{e}^{\mathrm{j} \Omega})$'\ + ' in ' + fb.fil[0]['plt_tUnit'] + r' $ \rightarrow $' tau_g = tau_g / fb.fil[0]['f_S'] #--------------------------------------------------------- self.ax.clear() # need to clear, doesn't overwrite line_tau_g, = self.ax.plot(F, tau_g, label = "Group Delay") #--------------------------------------------------------- self.ax.set_title(r'Group Delay $ \tau_g$') self.ax.set_xlabel(fb.fil[0]['plt_fLabel']) self.ax.set_ylabel(tau_str) # widen y-limits to suppress numerical inaccuracies when tau_g = constant self.ax.set_ylim([max(min(tau_g)-0.5,0), max(tau_g) + 0.5]) self.ax.set_xlim(fb.fil[0]['freqSpecsRange']) self.redraw() #------------------------------------------------------------------------------ def redraw(self): """ Redraw the canvas when e.g. the canvas size has changed """ self.mplwidget.redraw()