Source code for pyfda.plot_widgets.plot_impz

# -*- coding: utf-8 -*-
#
# This file is part of the pyFDA project hosted at https://github.com/chipmuenk/pyfda
#
# Copyright © pyFDA Project Contributors
# Licensed under the terms of the MIT License
# (see file LICENSE in root directory for details)

"""
Widget for plotting impulse and general transient responses
"""
import logging
logger = logging.getLogger(__name__)

import time
from pyfda.libs.compat import QWidget, pyqtSignal, QTabWidget, QVBoxLayout

import numpy as np
from numpy import pi
import scipy.signal as sig
from scipy.special import sinc
import matplotlib.patches as mpl_patches
from matplotlib.ticker import AutoMinorLocator

import pyfda.filterbroker as fb
import pyfda.libs.pyfda_fix_lib as fx
from pyfda.libs.pyfda_lib import (to_html, safe_eval, pprint_log, np_type, calc_ssb_spectrum,
        rect_bl, sawtooth_bl, triang_bl, comb_bl, calc_Hcomplex, safe_numexpr_eval)
from pyfda.libs.pyfda_qt_lib import (qget_cmb_box, qset_cmb_box, qstyle_widget,
                                     qadd_item_cmb_box, qdel_item_cmb_box)
from pyfda.pyfda_rc import params # FMT string for QLineEdit fields, e.g. '{:.3g}'
from pyfda.plot_widgets.mpl_widget import MplWidget, stems, no_plot

from pyfda.plot_widgets.plot_impz_ui import PlotImpz_UI

# TODO: "Home" calls redraw for botb mpl widgets
# TODO: changing the view on some widgets redraws h[n] unncessarily

classes = {'Plot_Impz':'y[n]'} #: Dict containing class name : display name

[docs]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 self.needs_calc = True # flag whether plots need to be recalculated self.needs_redraw = [True] * 2 # flag which plot needs to be redrawn self.error = False # initial setting for fixpoint simulation: self.fx_sim = qget_cmb_box(self.ui.cmb_sim_select, data=False) == 'Fixpoint' self.fx_sim_old = self.fx_sim self.tool_tip = "Impulse and transient response" self.tab_label = "y[n]" self.active_tab = 0 # index for active tab 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_plot_stmq = {'color':'darkgreen', 'linewidth':2, 'alpha':0.5} self.fmt_mkr_stmq = {'color':'darkgreen', 'alpha':0.5} self.fmt_stem_stim = params['mpl_stimuli'] self._construct_UI() #-------------------------------------------- # initialize routines and settings self.fx_select() # initialize fixpoint or float simulation self.impz() # initial calculation of stimulus and response and drawing def _construct_UI(self): """ Create the top level UI of the widget, consisting of matplotlib widget and control frame. """ #---------------------------------------------------------------------- # Define MplWidget for 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']) self.mplwidget_t.mplToolbar.a_he.setEnabled(True) self.mplwidget_t.mplToolbar.a_he.info = "manual/plot_impz.html" #---------------------------------------------------------------------- # Define 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']) self.mplwidget_f.mplToolbar.a_he.setEnabled(True) self.mplwidget_f.mplToolbar.a_he.info = "manual/plot_impz.html" #---------------------------------------------------------------------- # Tabbed layout with vertical tabs #---------------------------------------------------------------------- self.tabWidget = QTabWidget(self) self.tabWidget.addTab(self.mplwidget_t, "Time") self.tabWidget.setTabToolTip(0,"Impulse and transient response of filter") self.tabWidget.addTab(self.mplwidget_f, "Frequency") self.tabWidget.setTabToolTip(1,"Spectral representation of impulse or transient response") # 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.impz) self.ui.chk_scale_impz_f.clicked.connect(self.draw) self.ui.but_run.clicked.connect(self.impz) self.ui.chk_auto_run.clicked.connect(self.calc_auto) self.ui.chk_fx_scale.clicked.connect(self.draw) self.ui.but_fft_win.clicked.connect(self.ui.show_fft_win) # --- time domain plotting --- self.ui.cmb_plt_time_resp.currentIndexChanged.connect(self.draw) self.ui.cmb_plt_time_stim.currentIndexChanged.connect(self.draw) self.ui.cmb_plt_time_stmq.currentIndexChanged.connect(self.draw) self.ui.cmb_plt_time_spgr.currentIndexChanged.connect(self._spgr_cmb) self.ui.chk_log_time.clicked.connect(self.draw) self.ui.led_log_bottom_time.editingFinished.connect(self._log_bottom) self.ui.chk_log_spgr_time.clicked.connect(self.draw) self.ui.led_nfft_spgr_time.editingFinished.connect(self._spgr_params) self.ui.led_ovlp_spgr_time.editingFinished.connect(self._spgr_params) self.ui.cmb_mode_spgr_time.currentIndexChanged.connect(self.draw) self.ui.chk_byfs_spgr_time.clicked.connect(self.draw) self.ui.chk_fx_limits.clicked.connect(self.draw) self.ui.chk_win_time.clicked.connect(self.draw) # --- frequency domain plotting --- self.ui.cmb_plt_freq_resp.currentIndexChanged.connect(self.draw) self.ui.cmb_plt_freq_stim.currentIndexChanged.connect(self.draw) self.ui.cmb_plt_freq_stmq.currentIndexChanged.connect(self.draw) self.ui.chk_Hf.clicked.connect(self.draw) self.ui.chk_re_im_freq.clicked.connect(self.draw) 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_show_info_freq.clicked.connect(self.draw) #self.ui.chk_win_freq.clicked.connect(self.draw) 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 recalculation of current tab self.tabWidget.currentChanged.connect(self.draw) # passes number of active tab self.sig_rx.connect(self.process_sig_rx) # connect UI to widgets and signals upstream: self.ui.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------
[docs] def process_sig_rx(self, dict_sig=None): """ Process signals coming from - the navigation toolbars (time and freq.) - local widgets (impz_ui) and - plot_tab_widgets() (global signals) """ logger.debug("PROCESS_SIG_RX - needs_calc: {0} | vis: {1}\n{2}"\ .format(self.needs_calc, self.isVisible(), pprint_log(dict_sig))) if dict_sig['sender'] == __name__: logger.debug("Stopped infinite loop:\n{0}".format(pprint_log(dict_sig))) return self.error = False if 'closeEvent' in dict_sig: self.close_FFT_win() return # probably not needed # --- signals for fixpoint simulation --------------------------------- if 'fx_sim' in dict_sig: if dict_sig['fx_sim'] == 'specs_changed': self.needs_calc = True self.error = False qstyle_widget(self.ui.but_run, "changed") if self.isVisible(): self.impz() elif dict_sig['fx_sim'] == 'get_stimulus': """ - Select Fixpoint mode - Calculate stimuli, quantize and pass to dict_sig with `'fx_sim':'send_stimulus'` and `'fx_stimulus':<quantized stimulus array>`. Stimuli are scaled with the input fractional word length, i.e. with 2**WF (input) to obtain integer values """ self.needs_calc = True # always require recalculation when triggered externally self.needs_redraw = [True] * 2 self.error = False qstyle_widget(self.ui.but_run, "changed") self.fx_select("Fixpoint") if self.isVisible(): self.calc_stimulus() elif dict_sig['fx_sim'] == 'set_results': """ - Convert simulation results to integer and transfer them to the plotting routine """ logger.debug("Received fixpoint results.") self.draw_response_fx(dict_sig=dict_sig) elif dict_sig['fx_sim'] == 'error': self.needs_calc = True self.error = True qstyle_widget(self.ui.but_run, "error") return elif not dict_sig['fx_sim']: logger.error('Missing value for key "fx_sim".') else: logger.error('Unknown value "{0}" for "fx_sim" key\n'\ '\treceived from "{1}"'.format(dict_sig['fx_sim'], dict_sig['sender'])) # --- widget is visible, handle all signals except 'fx_sim' ----------- elif self.isVisible(): # all signals except 'fx_sim' if 'data_changed' in dict_sig or 'specs_changed' in dict_sig or self.needs_calc: # update number of data points in impz_ui and FFT window # needed when e.g. FIR filter order has changed. Don't emit a signal. self.ui.update_N(emit=False) self.needs_calc = True qstyle_widget(self.ui.but_run, "changed") self.impz() elif 'view_changed' in dict_sig: if dict_sig['view_changed'] == 'f_S': self.ui.recalc_freqs() self.draw() else: self.draw() elif 'ui_changed' in dict_sig: # exclude those ui elements / events that don't require a recalculation if dict_sig['ui_changed'] in {'win'}: self.draw() elif dict_sig['ui_changed'] in {'resized','tab'}: pass else: # all the other ui elements are treated here self.needs_calc = True qstyle_widget(self.ui.but_run, "changed") self.impz() 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 else: # invisible if 'data_changed' in dict_sig or 'specs_changed' in dict_sig: self.needs_calc = True
# elif 'fx_sim' in dict_sig and dict_sig['fx_sim'] == 'get_stimulus': # self.needs_calc = True # always require recalculation when triggered externally # qstyle_widget(self.ui.but_run, "changed") # self.fx_select("Fixpoint") # ============================================================================= # Simulation: Calculate stimulus, response and draw them # =============================================================================
[docs] def calc_auto(self, autorun=None): """ Triggered when checkbox "Autorun" is clicked. Enable or disable the "Run" button depending on the setting of the checkbox. When checkbox is checked (`autorun == True` passed via signal- slot connection), automatically run `impz()`. """ self.ui.but_run.setEnabled(not autorun) if autorun: self.impz()
[docs] def impz(self, arg=None): """ Triggered by: - construct_UI() [Initialization] - Pressing "Run" button, passing button state as a boolean - Activating "Autorun" via `self.calc_auto()` - 'fx_sim' : 'specs_changed' - Calculate response and redraw it. Stimulus and response are only calculated if `self.needs_calc == True`. """ # allow scaling the frequency response from pure impulse (no DC, no noise) self.ui.chk_scale_impz_f.setEnabled((self.ui.noi == 0 or self.ui.cmbNoise.currentText() == 'None')\ and self.ui.DC == 0) self.fx_select() # check for fixpoint setting and update if needed if type(arg) == bool: # but_run has been pressed self.needs_calc = True # force recalculation when but_run is pressed elif not self.ui.chk_auto_run.isChecked(): return if self.needs_calc: logger.debug("Calc impz started!") if self.fx_sim: # start a fixpoint simulation self.sig_tx.emit({'sender':__name__, 'fx_sim':'init'}) return self.calc_stimulus() self.calc_response() if self.error: return self.needs_calc = False self.needs_redraw = [True] * 2 if self.needs_redraw[self.tabWidget.currentIndex()]: logger.debug("Redraw impz started!") self.draw() self.needs_redraw[self.tabWidget.currentIndex()] = False qstyle_widget(self.ui.but_run, "normal")
# =============================================================================
[docs] def fx_select(self, fx=None): """ Select between fixpoint and floating point simulation. Parameter `fx` can be: - str "Fixpoint" or "Float" when called directly - int 0 or 1 when triggered by changing the index of combobox `self.ui.cmb_sim_select` (signal-slot-connection) In both cases, the index of the combobox is updated according to the passed argument. If the index has been changed since last time, `self.needs_calc` is set to True and the run button is set to "changed". When fixpoint simulation is selected, all corresponding widgets are made visible. `self.fx_sim` is set to True. If `self.fx_sim` has changed, `self.needs_calc` is set to True. """ logger.debug("start fx_select") if fx in {0, 1}: # connected to index change of combo box pass elif fx in {"Float", "Fixpoint"}: # direct function call qset_cmb_box(self.ui.cmb_sim_select, fx) elif fx is None: pass else: logger.error('Unknown argument "{0}".'.format(fx)) return self.fx_sim = qget_cmb_box(self.ui.cmb_sim_select, data=False) == 'Fixpoint' self.ui.cmb_plt_freq_stmq.setVisible(self.fx_sim) self.ui.lbl_plt_freq_stmq.setVisible(self.fx_sim) self.ui.cmb_plt_time_stmq.setVisible(self.fx_sim) self.ui.lbl_plt_time_stmq.setVisible(self.fx_sim) self.ui.chk_fx_scale.setVisible(self.fx_sim) self.ui.chk_fx_limits.setVisible(self.fx_sim) if self.fx_sim: qadd_item_cmb_box(self.ui.cmb_plt_time_spgr, "x_q[n]") else: qdel_item_cmb_box(self.ui.cmb_plt_time_spgr, "x_q[n]") if self.fx_sim != self.fx_sim_old: qstyle_widget(self.ui.but_run, "changed") # even if nothing else has changed, stimulus and response must be recalculated self.needs_calc = True self.fx_sim_old = self.fx_sim
#------------------------------------------------------------------------------
[docs] def calc_stimulus(self): """ (Re-)calculate stimulus `self.x` """ self.n = np.arange(self.ui.N_end) phi1 = self.ui.phi1 / 180 * pi phi2 = self.ui.phi2 / 180 * pi # calculate stimuli x[n] ============================================== self.H_str = '' self.title_str = "" if self.ui.stim == "Impulse": if np_type(self.ui.A1) == complex: A_type = complex else: A_type = float self.x = np.zeros(self.ui.N_end, dtype=A_type) 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'Zero Input System Response' 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*pi * self.n * self.ui.f1 + phi1) +\ self.ui.A2 * np.cos(2*pi * self.n * self.ui.f2 + phi2) self.title_str += r'Cosine Signal' elif self.ui.stim == "Sine": self.x = self.ui.A1 * np.sin(2*pi * self.n * self.ui.f1 + phi1) +\ self.ui.A2 * np.sin(2*pi * self.n * self.ui.f2 + phi2) self.title_str += r'Sinusoidal Signal ' elif self.ui.stim == "Sinc": self.x = self.ui.A1 * sinc(2 * (self.n - self.ui.N//2) * self.ui.f1 + phi1) +\ self.ui.A2 * sinc(2 * (self.n - self.ui.N//2) * self.ui.f2 + phi2) self.title_str += r'Sinc Signal ' elif self.ui.stim == "Chirp": self.x = self.ui.A1 * sig.chirp(self.n, self.ui.f1, self.ui.N_end, self.ui.f2, method=self.ui.chirp_method.lower(), phi=phi1) self.title_str += self.ui.chirp_method + ' Chirp Signal' elif self.ui.stim == "Triang": if self.ui.chk_stim_bl.isChecked(): self.x = self.ui.A1 * triang_bl(2*pi * self.n * self.ui.f1 + phi1) self.title_str += r'Bandlim. Triangular Signal' else: self.x = self.ui.A1 * sig.sawtooth(2*pi * self.n * self.ui.f1 + phi1, width=0.5) self.title_str += r'Triangular Signal' elif self.ui.stim == "Saw": if self.ui.chk_stim_bl.isChecked(): self.x = self.ui.A1 * sawtooth_bl(2*pi * self.n * self.ui.f1 + phi1) self.title_str += r'Bandlim. Sawtooth Signal' else: self.x = self.ui.A1 * sig.sawtooth(2*pi * self.n * self.ui.f1 + phi1) self.title_str += r'Sawtooth Signal' elif self.ui.stim == "Rect": if self.ui.chk_stim_bl.isChecked(): self.x = self.ui.A1 * rect_bl(2*pi * self.n * self.ui.f1 + phi1, duty=self.ui.stim_par1) self.title_str += r'Bandlimited Rect. Signal' else: self.x = self.ui.A1 * sig.square(2*pi * self.n * self.ui.f1 + phi1, duty=self.ui.stim_par1) self.title_str += r'Rect. Signal' elif self.ui.stim == "Comb": self.x = self.ui.A1 * comb_bl(2*pi * self.n * self.ui.f1 + phi1) self.title_str += r'Bandlim. Comb Signal' elif self.ui.stim == "AM": self.x = self.ui.A1 * np.sin(2*pi * self.n * self.ui.f1 + phi1)\ * self.ui.A2 * np.sin(2*pi * self.n * self.ui.f2 + phi2) self.title_str += r'AM Signal $A_1 \sin(2 \pi n f_1 + \varphi_1) \cdot A_2 \sin(2 \pi n f_2 + \varphi_2)$' elif self.ui.stim == "PM / FM": self.x = self.ui.A1 * np.sin(2*pi * self.n * self.ui.f1 + phi1 +\ self.ui.A2 * np.sin(2*pi * self.n * self.ui.f2 + phi2)) self.title_str += r'PM / FM Signal $A_1 \sin(2 \pi n f_1 + \varphi_1 + A_2 \sin(2 \pi n f_2 + \varphi_2))$' elif self.ui.stim == "Formula": param_dict = {"A1":self.ui.A1, "A2":self.ui.A2, "f1":self.ui.f1, "f2":self.ui.f2, "phi1":self.ui.phi1, "phi2":self.ui.phi2, "f_S":fb.fil[0]['f_S'], "n":self.n} self.x = safe_numexpr_eval(self.ui.stim_formula, (self.ui.N_end,), param_dict) self.title_str += r'Formula Defined Signal' else: logger.error('Unknown stimulus format "{0}"'.format(self.ui.stim)) return # Add noise to stimulus noi = 0 if self.ui.noise == "gauss": noi = self.ui.noi * np.random.randn(len(self.x)) self.title_str += r' + Gaussian Noise' elif self.ui.noise == "uniform": noi = self.ui.noi * (np.random.rand(len(self.x))-0.5) self.title_str += r' + Uniform Noise' elif self.ui.noise == "prbs": noi = self.ui.noi * 2 * (np.random.randint(0, 2, len(self.x))-0.5) self.title_str += r' + PRBS Noise' if type(self.ui.noi) == complex: self.x = self.x.astype(complex) + noi else: self.x += noi # Add DC to stimulus when visible / enabled if self.ui.ledDC.isVisible: if type(self.ui.DC) == complex: self.x = self.x.astype(complex) + self.ui.DC else: self.x += self.ui.DC if self.ui.DC != 0: self.title_str += r' + DC' if self.fx_sim: self.title_str = r'$Fixpoint$ ' + self.title_str self.q_i = fx.Fixed(fb.fil[0]['fxqc']['QI']) # setup quantizer for input quantization self.q_i.setQobj({'frmt':'dec'}) # always use integer decimal format if np.any(np.iscomplex(self.x)): logger.warning("Complex stimulus: Only its real part will be processed by the fixpoint filter!") self.x_q = self.q_i.fixp(self.x.real) self.sig_tx.emit({'sender':__name__, 'fx_sim':'send_stimulus', 'fx_stimulus':np.round(self.x_q * (1 << self.q_i.WF)).astype(int)}) logger.debug("fx stimulus sent") self.needs_redraw[:] = [True] * 2
#------------------------------------------------------------------------------
[docs] def calc_response(self): """ (Re-)calculate ideal filter response `self.y` from stimulus `self.x` and the filter coefficients using `lfilter()`, `sosfilt()` or `filtfilt()`. Set the flag `self.cmplx` when response `self.y` or stimulus `self.x` are complex and make warning field visible. """ 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.debug("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[:] = [True] * 2 # Calculate imag. and real components from response self.cmplx = np.any(np.iscomplex(self.y)) or np.any(np.iscomplex(self.x)) self.ui.lbl_stim_cmplx_warn.setVisible(self.cmplx)
#------------------------------------------------------------------------------
[docs] def draw_response_fx(self, dict_sig=None): """ Get Fixpoint results and plot them """ if self.needs_calc: self.needs_redraw = [True] * 2 #t_draw_start = time.process_time() self.y = np.asarray(dict_sig['fx_results']) if self.y is None: qstyle_widget(self.ui.but_run, "error") self.needs_calc = True else: self.needs_calc = False self.draw() qstyle_widget(self.ui.but_run, "normal") self.sig_tx.emit({'sender':__name__, 'fx_sim':'finish'})
#------------------------------------------------------------------------------
[docs] def calc_fft(self): """ (Re-)calculate FFTs of stimulus `self.X`, quantized stimulus `self.X_q` and response `self.Y` using the window function `self.ui.win`. """ # 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: # multiply the time signal with window function x_win = self.x[self.ui.N_start:self.ui.N_end] * self.ui.win # calculate absolute value and scale by N_FFT self.X = np.fft.fft(x_win) / self.ui.N #self.X[0] = self.X[0] * np.sqrt(2) # correct value at DC if self.fx_sim: # same for fixpoint simulation x_q_win = self.q_i.fixp(self.x[self.ui.N_start:self.ui.N_end]) * self.ui.win self.X_q = np.fft.fft(x_q_win) / self.ui.N #self.X_q[0] = self.X_q[0] * np.sqrt(2) # correct value at DC 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.fft.fft(y_win) / self.ui.N #self.Y[0] = self.Y[0] * np.sqrt(2) # correct value at DC # 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 # redraw of frequency widget needed
############################################################################### # PLOTTING ###############################################################################
[docs] def draw(self, arg=None): """ (Re-)draw the figure without recalculation. When triggered by a signal- slot connection from a button, combobox etc., arg is a boolean or an integer representing the state of the widget. In this case, `needs_redraw` is set to True. """ if type(arg) is not None: self.needs_redraw = [True] * 2 if not hasattr(self, 'cmplx'): # has response been calculated yet? logger.error("Response should have been calculated by now!") return if fb.fil[0]['freq_specs_unit'] == 'k': f_unit = '' self.ui.lblFreq1.setText(self.ui.txtFreq1_k) self.ui.lblFreq2.setText(self.ui.txtFreq2_k) else: f_unit = fb.fil[0]['plt_fUnit'] self.ui.lblFreq1.setText(self.ui.txtFreq1_f) self.ui.lblFreq2.setText(self.ui.txtFreq2_f) if f_unit in {"f_S", "f_Ny"}: unit_frmt = "i" # italic else: unit_frmt = None # don't print units like kHz in italic self.ui.lblFreqUnit1.setText(to_html(f_unit, frmt=unit_frmt)) self.ui.lblFreqUnit2.setText(to_html(f_unit, frmt=unit_frmt)) self.t = self.n * fb.fil[0]['T_S'] # self.ui.load_fs() self.scale_i = self.scale_o = 1 self.fx_min = -1. self.fx_max = 1. if self.fx_sim: # fixpoint simulation enabled -> scale stimulus and response try: if self.ui.chk_fx_scale.isChecked(): # display stimulus and response as integer values: # - multiply stimulus by 2 ** WF # - display response unscaled self.scale_i = 1 << fb.fil[0]['fxqc']['QI']['WF'] self.fx_min = - (1 << fb.fil[0]['fxqc']['QO']['W']-1) self.fx_max = -self.fx_min - 1 else: # display values scaled as "real world values" self.scale_o = 1. / (1 << fb.fil[0]['fxqc']['QO']['WF']) self.fx_min = -(1 << fb.fil[0]['fxqc']['QO']['WI']) self.fx_max = -self.fx_min - self.scale_o except AttributeError as e: logger.error("Attribute error: {0}".format(e)) except TypeError as e: logger.error("Type error: 'fxqc_dict'={0},\n{1}".format(fb.fil[0]['fxqc'], e)) except ValueError as e: logger.error("Value error: {0}".format(e)) idx = self.tabWidget.currentIndex() if idx == 0 and self.needs_redraw[0]: self.draw_time() elif idx == 1 and self.needs_redraw[1]: self.draw_freq() self.ui.show_fft_win()
#------------------------------------------------------------------------------ def _spgr_params(self): """ Update parameters for spectrogram """ self.ui.nfft_spgr_time = safe_eval(self.ui.led_nfft_spgr_time.text(), self.ui.nfft_spgr_time, return_type='int', sign='pos') self.ui.led_nfft_spgr_time.setText(str(self.ui.nfft_spgr_time)) self.ui.ovlp_spgr_time = safe_eval(self.ui.led_ovlp_spgr_time.text(), self.ui.ovlp_spgr_time, return_type='int', sign='poszero') self.ui.led_ovlp_spgr_time.setText(str(self.ui.ovlp_spgr_time)) if self.ui.nfft_spgr_time <= self.ui.ovlp_spgr_time: logger.warning("N_OVLP must be less than N_FFT!") self.draw() #------------------------------------------------------------------------------ def _spgr_cmb(self): """ Update spectrogram ui """ spgr_en = self.ui.cmb_plt_time_spgr.currentText() != "None" self.ui.lbl_log_spgr_time.setVisible(spgr_en) self.ui.chk_log_spgr_time.setVisible(spgr_en) self.ui.lbl_nfft_spgr_time.setVisible(spgr_en) self.ui.led_nfft_spgr_time.setVisible(spgr_en) self.ui.lbl_ovlp_spgr_time.setVisible(spgr_en) self.ui.led_ovlp_spgr_time.setVisible(spgr_en) self.ui.lbl_mode_spgr_time.setVisible(spgr_en) self.ui.cmb_mode_spgr_time.setVisible(spgr_en) self.ui.lbl_byfs_spgr_time.setVisible(spgr_en) self.ui.chk_byfs_spgr_time.setVisible(spgr_en) self.draw() #------------------------------------------------------------------------------ def _log_bottom(self): """ Select / deselect log. mode for time domain and update self.ui.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.bottom_t = safe_eval(self.ui.led_log_bottom_time.text(), self.ui.bottom_t, return_type='float', sign='neg') self.ui.led_log_bottom_time.setText(str(self.ui.bottom_t)) self.draw() #------------------------------------------------------------------------------ def _log_mode_freq(self): """ Select / deselect log. mode for frequency domain and update self.ui.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) if log: self.ui.bottom_f = safe_eval(self.ui.led_log_bottom_freq.text(), self.ui.bottom_f, return_type='float', sign='neg') self.ui.led_log_bottom_freq.setText(str(self.ui.bottom_f)) else: self.ui.bottom_f = 0 self.draw() #------------------------------------------------------------------------------
[docs] def draw_data(self, plt_style, ax, x, y, bottom=0, label='', plt_fmt=None, mkr=False, mkr_fmt=None, **args): """ Plot x, y data (numpy arrays with equal length) in a plot style defined by `plt_style`. Parameters ---------- plt_style : str one of "line", "stem", "step", "dots" ax : matplotlib axis Handle to the axis where signal is x : array-like x-axis: time or frequency data y : array-like y-data bottom : float Bottom line for stem plot. The default is 0. label : str Plot label plt_fmt : dict General styles (color, linewidth etc.) for plotting. The default is None. mkr : bool Plot a marker for every data point if enabled mkr_fmt : dict Marker styles args : dictionary with additional keys and values. As they might not be compatible with every plot style, they have to be added individually Returns ------- None """ if plt_fmt is None: plt_fmt = {} if plt_style == "line": ax.plot(x,y, label=label, **plt_fmt) elif plt_style == "stem": stems(x,y, ax=ax, bottom=bottom, label=label, **plt_fmt) elif plt_style == "step": ax.plot(x,y, drawstyle='steps-mid', label=label, **plt_fmt) elif plt_style == "dots": ax.scatter(x,y, label=label, **plt_fmt) else: pass if mkr: if 'marker' in args: ax.scatter(x,y, **mkr_fmt, marker=args['marker']) else: ax.scatter(x, y, **mkr_fmt)
#================ Plotting routine time domain ========================= def _init_axes_time(self): """ Clear the axes of the time domain matplotlib widgets and (re)draw the plots. """ self.plt_time_resp = qget_cmb_box(self.ui.cmb_plt_time_resp, data=False).lower().replace("*", "") self.plt_time_resp_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_time_resp, data=False) self.plt_time_stim = qget_cmb_box(self.ui.cmb_plt_time_stim, data=False).lower().replace("*", "") self.plt_time_stim_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_time_stim, data=False) self.plt_time_stmq = qget_cmb_box(self.ui.cmb_plt_time_stmq, data=False).lower().replace("*", "") self.plt_time_stmq_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_time_stmq, data=False) self.plt_time_spgr = qget_cmb_box(self.ui.cmb_plt_time_spgr, data=False).lower() self.spgr = self.plt_time_spgr != "none" self.plt_time = self.plt_time_resp != "none" or self.plt_time_stim != "none"\ or (self.plt_time_stmq != "none" and self.fx_sim)\ or self.spgr or self.ui.chk_win_time.isChecked() self.mplwidget_t.fig.clf() # clear figure with axes if self.plt_time: num_subplots = 1 + self.cmplx + self.spgr # return a one-dimensional list with num_subplots axes self.axes_time = self.mplwidget_t.fig.subplots(nrows=num_subplots, ncols=1, sharex=True, squeeze = False)[:,0] self.ax_r = self.axes_time[0] self.ax_r.cla() if self.cmplx: self.ax_i = self.axes_time[1] self.ax_i.cla() self.mplwidget_t.fig.align_ylabels() if self.spgr: self.ax_s = self.axes_time[-1] # assign last axis if self.ACTIVE_3D: # not implemented / tested yet self.ax3d = self.mplwidget_t.fig.add_subplot(111, projection='3d') for ax in self.axes_time: ax.xaxis.tick_bottom() # remove axis ticks on top ax.yaxis.tick_left() # remove axis ticks right ax.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks ax.yaxis.set_minor_locator(AutoMinorLocator()) #------------------------------------------------------------------------------
[docs] def draw_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 if not self.H_str or self.H_str[1] != 'h': # '$h... = some impulse response, don't change self.H_str = '' if qget_cmb_box(self.ui.cmb_plt_time_stim, data=False).lower() != "none": self.H_str += r'$x$, ' if qget_cmb_box(self.ui.cmb_plt_time_stmq, data=False).lower() != "none" and self.fx_sim: self.H_str += r'$x_Q$, ' if qget_cmb_box(self.ui.cmb_plt_time_resp, data=False).lower() != "none": self.H_str += r'$y$' self.H_str = self.H_str.rstrip(', ') mkfmt_i = 'd' self._init_axes_time() if self.fx_sim: # fixpoint simulation enabled -> scale stimulus and response x_q = self.x_q * self.scale_i if self.ui.chk_log_time.isChecked(): x_q = np.maximum(20 * np.log10(abs(x_q)), self.ui.bottom_t) logger.debug("self.scale I:{0} O:{1}".format(self.scale_i, self.scale_o)) else: x_q = None x = self.x * self.scale_i y = self.y * self.scale_o if self.cmplx: x_r = x.real x_i = x.imag y_r = y.real y_i = y.imag lbl_x_r = "$x_r[n]$" lbl_x_i = "$x_i[n]$" lbl_y_r = "$y_r[n]$" lbl_y_i = "$y_i[n]$" else: x_r = x.real x_i = None y_r = y y_i = None lbl_x_r = "$x[n]$" lbl_y_r = "$y[n]$" if self.ui.chk_log_time.isChecked(): # log. scale for stimulus / response time domain bottom_t = self.ui.bottom_t win = np.maximum(20 * np.log10(abs(self.ui.win)), self.ui.bottom_t) x_r = np.maximum(20 * np.log10(abs(x_r)), self.ui.bottom_t) y_r = np.maximum(20 * np.log10(abs(y_r)), self.ui.bottom_t) if self.cmplx: x_i = np.maximum(20 * np.log10(abs(x_i)), self.ui.bottom_t) y_i = np.maximum(20 * np.log10(abs(y_i)), self.ui.bottom_t) H_i_str = r'$|\Im\{$' + self.H_str + r'$\}|$' + ' in dBV' H_str = r'$|\Re\{$' + self.H_str + r'$\}|$' + ' in dBV' else: H_str = '$|$' + self.H_str + '$|$ in dBV' fx_min = 20*np.log10(abs(self.fx_min)) fx_max = fx_min else: bottom_t = 0 fx_max = self.fx_max fx_min = self.fx_min win = self.ui.win if self.cmplx: H_i_str = r'$\Im\{$' + self.H_str + r'$\}$ in V' H_str = r'$\Re\{$' + self.H_str + r'$\}$ in V' else: H_str = self.H_str + ' in V' if self.ui.chk_fx_limits.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 ---------------------------------- self.draw_data(self.plt_time_stim, self.ax_r, self.t[self.ui.N_start:], x_r[self.ui.N_start:], label=lbl_x_r, bottom=bottom_t, plt_fmt=self.fmt_plot_stim, mkr=self.plt_time_stim_mkr, mkr_fmt=self.fmt_mkr_stim) #-------------- Stimulus <q> plot -------------------------------- if x_q is not None and self.plt_time_stmq != "none": self.draw_data(self.plt_time_stmq, self.ax_r, self.t[self.ui.N_start:], x_q[self.ui.N_start:], label='$x_q[n]$', bottom=bottom_t, plt_fmt=self.fmt_plot_stmq, mkr=self.plt_time_stmq_mkr, mkr_fmt=self.fmt_mkr_stmq) # --------------- Response plot ---------------------------------- self.draw_data(self.plt_time_resp, self.ax_r, self.t[self.ui.N_start:], y_r[self.ui.N_start:], label=lbl_y_r, bottom=bottom_t, plt_fmt=self.fmt_plot_resp, mkr=self.plt_time_resp_mkr, mkr_fmt=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_name) # --------------- LEGEND (real part) ---------------------------------- if self.plt_time: self.ax_r.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.7) # --------------- Complex response ---------------------------------- if self.cmplx and (self.plt_time_resp != "none" or self.plt_time_stim != "none"): # --- imag. part of response ----- self.draw_data(self.plt_time_resp, self.ax_i, self.t[self.ui.N_start:], y_i[self.ui.N_start:], label=lbl_y_i, bottom=bottom_t, plt_fmt=self.fmt_plot_resp, mkr=self.plt_time_resp_mkr, mkr_fmt=self.fmt_mkr_resp, marker=mkfmt_i) # --- imag. part of stimulus ----- self.draw_data(self.plt_time_stim, self.ax_i, self.t[self.ui.N_start:], x_i[self.ui.N_start:], label=lbl_x_i, bottom=bottom_t, plt_fmt=self.fmt_plot_stim, mkr=self.plt_time_stim_mkr, mkr_fmt=self.fmt_mkr_stim, marker=mkfmt_i) # --- labels and markers ----- # 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 $') # common x-axis self.ax_i.set_ylabel(H_i_str + r'$\rightarrow $') self.ax_i.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.7) #else: # self.ax_r.set_xlabel(fb.fil[0]['plt_tLabel']) self.ax_r.set_ylabel(H_str + r'$\rightarrow $') # --------------- Spectrogram ----------------------------------------- if self.spgr: if self.plt_time_spgr == "x[n]": s = x[self.ui.N_start:] sig_lbl = 'X' elif self.plt_time_spgr == "x_q[n]": s = self.x_q[self.ui.N_start:] sig_lbl = 'X_Q' elif self.plt_time_spgr == "y[n]": s = y[self.ui.N_start:] sig_lbl = 'Y' else: s = None sig_lbl = 'None' spgr_args = r"$({0}, {1})$".format(fb.fil[0]['plt_tLabel'][1], fb.fil[0]['plt_fLabel'][1]) # ------ onesided / twosided ------------ if fb.fil[0]['freqSpecsRangeType'] == 'half': sides = 'onesided' else: sides = 'twosided' # ------- Unit / Mode ---------------------- mode = qget_cmb_box(self.ui.cmb_mode_spgr_time, data=True) self.ui.lbl_byfs_spgr_time.setVisible(mode=='psd') self.ui.chk_byfs_spgr_time.setVisible(mode=='psd') spgr_pre = "" if self.ui.chk_log_spgr_time.isChecked(): dB_unit = "dB" else: dB_unit = "" if mode == "psd": spgr_symb = r"$S_{{{0}}}$".format(sig_lbl.lower()+sig_lbl.lower()) # Power Spectral Density if self.ui.chk_byfs_spgr_time.isChecked(): # scale result by f_S spgr_unit = r" in {0}W / Hz".format(dB_unit) else: spgr_unit = r" in {0}W".format(dB_unit) elif mode in {"magnitude", "complex"}: # "complex" cannot be plotted directly spgr_pre = r"|" spgr_symb = "${0}$".format(sig_lbl) spgr_unit = r"| in {0}V".format(dB_unit) elif mode in {"angle", "phase"}: spgr_unit = r" in rad" spgr_symb = "${0}$".format(sig_lbl) spgr_pre = r"$\angle$" # must be linear if mode is 'angle' or 'phase': self.ui.chk_log_spgr_time.blockSignals(True) self.ui.chk_log_spgr_time.setChecked(False) self.ui.chk_log_spgr_time.blockSignals(False) else: logger.warning("Unknown spectrogram mode {0}".format(mode)) mode = None # ------- lin / log ---------------------- if self.ui.chk_log_spgr_time.isChecked(): scale = 'dB' # 10 log10 for 'psd', otherwise 20 log10 bottom_spgr = self.ui.bottom_t else: scale = 'linear' bottom_spgr = 0 t_range = (self.t[self.ui.N_start], self.t[-1]) # hidden images: https://scipython.com/blog/hidden-images-in-spectrograms/ # ============================================================================= # f, t, Sxx = sig.spectrogram(s, fb.fil[0]['f_S'], # nperseg=None, noverlap=None, nfft=None, # return_onesided = fb.fil[0]['freqSpecsRangeType'] == 'half', # scaling='density',mode='psd') # # mode: 'psd', 'complex','magnitude','angle', 'phase' # ============================================================================= Sxx,f,t,im = self.ax_s.specgram(s, Fs=fb.fil[0]['f_S'], NFFT=self.ui.nfft_spgr_time, noverlap=self.ui.ovlp_spgr_time, pad_to=None, xextent=t_range, sides=sides, scale_by_freq=self.ui.chk_byfs_spgr_time.isChecked(), mode=mode, scale=scale, vmin=bottom_spgr, cmap=None) # Fs : sampling frequency for scaling # window: callable or ndarray, default window_hanning # NFFT : data points for each block # pad_to: create zero-padding # xextent: image extent along x-axis; None or (xmin, xmax) # scale_by_freq: True scales power spectral density by f_S # col_mesh = self.ax_s.pcolormesh(t, np.fft.fftshift(f), # np.fft.fftshift(Sxx, axes=0), shading='gouraud') # *fb.fil[0]['f_S'] #self.ax_s.colorbar(col_mesh) cbar = self.mplwidget_t.fig.colorbar(im, ax=self.ax_s, aspect=30, pad=0.005) cbar.ax.set_ylabel(spgr_pre + spgr_symb + spgr_args + spgr_unit) self.ax_s.set_ylabel(fb.fil[0]['plt_fLabel']) # --------------- 3D Complex ----------------------------------------- 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_r[i], y_r[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_r[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') # --------------- Title and common labels ---------------------------- self.axes_time[-1].set_xlabel(fb.fil[0]['plt_tLabel']) self.axes_time[0].set_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) self.redraw() # redraw currently active mplwidget self.needs_redraw[0] = False
#========================================================================= # Frequency Plots #========================================================================= def _init_axes_freq(self): """ Clear the axes of the frequency domain matplotlib widgets and calculate the fft """ self.plt_freq_resp = qget_cmb_box(self.ui.cmb_plt_freq_resp, data=False).lower().replace("*", "") self.plt_freq_resp_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_freq_resp, data=False) self.plt_freq_stim = qget_cmb_box(self.ui.cmb_plt_freq_stim, data=False).lower().replace("*", "") self.plt_freq_stim_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_freq_stim, data=False) self.plt_freq_stmq = qget_cmb_box(self.ui.cmb_plt_freq_stmq, data=False).lower().replace("*", "") self.plt_freq_stmq_mkr = "*" in qget_cmb_box(self.ui.cmb_plt_freq_stmq, data=False) self.plt_freq_enabled = self.plt_freq_stim != "none" or self.plt_freq_stmq != "none"\ or 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 self.mplwidget_f.fig.clf() # clear figure with axes en_re_im_f = self.ui.chk_re_im_freq.isChecked() num_subplots_f = 1 + en_re_im_f self.axes_f = self.mplwidget_f.fig.subplots(nrows=num_subplots_f, ncols=1, sharex=True, squeeze = False)[:,0] self.ax_f1 = self.axes_f[0] #for ax in self.axes_f: # 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_f1_noise = self.ax_f1.twinx() self.ax_f1_noise.is_twin = True self.ax_f1.xaxis.tick_bottom() # remove axis ticks on top self.ax_f1.yaxis.tick_left() # remove axis ticks right self.ax_f1.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax_f1.yaxis.set_minor_locator(AutoMinorLocator()) if en_re_im_f: self.ax_f2 = self.axes_f[1] self.ax_f2.xaxis.tick_bottom() # remove axis ticks on top self.ax_f2.yaxis.tick_left() # remove axis ticks right self.ax_f2.xaxis.set_minor_locator(AutoMinorLocator()) # enable minor ticks self.ax_f2.yaxis.set_minor_locator(AutoMinorLocator()) self.calc_fft()
[docs] def draw_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" plt_stimulus_q = self.plt_freq_stmq != "none" and self.fx_sim en_re_im_f = self.ui.chk_re_im_freq.isChecked() H_F_str = "" ejO_str = r"$(\mathrm{e}^{\mathrm{j} \Omega})$" if self.plt_freq_enabled or self.ui.chk_Hf.isChecked(): if plt_stimulus: H_F_str += r'$X$, ' if plt_stimulus_q: H_F_str += r'$X_Q$, ' if plt_response: H_F_str += r'$Y$, ' if self.ui.chk_Hf.isChecked(): H_F_str += r'$H_{id}$, ' H_F_str = H_F_str.rstrip(', ') + ejO_str F_range = fb.fil[0]['freqSpecsRange'] if fb.fil[0]['freq_specs_unit'] == 'k': # By default, k = params['N_FFT'] which is used for the calculation # of the non-transient tabs and for F_id / H_id here. # Here, the frequency axes must be scaled to fit the number of # frequency points self.ui.N F_range = [f * self.ui.N / fb.fil[0]['f_max'] for f in F_range] f_max = self.ui.N else: f_max = fb.fil[0]['f_max'] # freqz-based ideal frequency response: F_id, H_id = calc_Hcomplex(fb.fil[0], params['N_FFT'], True, fs=f_max) # frequency vector for FFT-based frequency plots: F = np.fft.fftfreq(self.ui.N, d=1. / f_max) #----------------------------------------------------------------- # Scale frequency response and calculate power #----------------------------------------------------------------- # - Scale signals # - Calculate total power P from FFT, corrected by window equivalent noise # bandwidth and fixpoint scaling (scale_i / scale_o) # - Correct scale for single-sided spectrum # - Scale impulse response with N_FFT to calculate frequency response if requested if self.ui.chk_scale_impz_f.isEnabled() and self.ui.stim == "Impulse"\ and self.ui.chk_scale_impz_f.isChecked(): freq_resp = True # calculate frequency response from impulse response scale_impz = self.ui.N else: freq_resp = False scale_impz = 1. if plt_stimulus: # scale display of frequency response Px = np.sum(np.square(np.abs(self.X))) * scale_impz / self.ui.nenbw if fb.fil[0]['freqSpecsRangeType'] == 'half' and not freq_resp: X = calc_ssb_spectrum(self.X) * self.scale_i * scale_impz else: X = self.X * self.scale_i * scale_impz if plt_stimulus_q: Pxq = np.sum(np.square(np.abs(self.X_q))) * scale_impz / self.ui.nenbw if fb.fil[0]['freqSpecsRangeType'] == 'half' and not freq_resp: X_q = calc_ssb_spectrum(self.X_q) * self.scale_i * scale_impz else: X_q = self.X_q * self.scale_i * scale_impz if plt_response: Py = np.sum(np.square(np.abs(self.Y * self.scale_o))) * scale_impz / self.ui.nenbw if fb.fil[0]['freqSpecsRangeType'] == 'half' and not freq_resp: Y = calc_ssb_spectrum(self.Y) * self.scale_o * scale_impz else: Y = self.Y * self.scale_o * scale_impz #----------------------------------------------------------------- # Scale and shift frequency range #----------------------------------------------------------------- if fb.fil[0]['freqSpecsRangeType'] == 'sym': # display -f_S/2 ... f_S/2 -> shift X, Y and F using fftshift() if plt_response: Y = np.fft.fftshift(Y) if plt_stimulus: X = np.fft.fftshift(X) if plt_stimulus_q: X_q = np.fft.fftshift(X_q) F = np.fft.fftshift(F) # shift H_id and F_id by f_S/2 F_id -= f_max/2 H_id = np.fft.fftshift(H_id) if not freq_resp: H_id /= 2 elif fb.fil[0]['freqSpecsRangeType'] == 'half': # display 0 ... f_S/2 -> 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 plt_stimulus_q: X_q = X_q[0:self.ui.N//2] F = F[0:self.ui.N//2] F_id = F_id[0:params['N_FFT']//2] H_id = H_id[0:params['N_FFT']//2] else: # fb.fil[0]['freqSpecsRangeType'] == 'whole' # display 0 ... f_S -> shift frequency axis F = np.fft.fftshift(F) + f_max/2. if not freq_resp: H_id /= 2 #----------------------------------------------------------------- # Calculate log FFT and power if selected, set units #----------------------------------------------------------------- if self.ui.chk_log_freq.isChecked(): unit = " in dBV" unit_P = "dBW" unit_nenbw = "dB" unit_cgain = "dB" H_F_pre = "|" H_F_post = "|" nenbw = 10 * np.log10(self.ui.nenbw) cgain = 20 * np.log10(self.ui.cgain) if plt_stimulus: Px = 10*np.log10(Px) if en_re_im_f: X_r = np.maximum(20 * np.log10(np.abs(X.real)), self.ui.bottom_f) X_i = np.maximum(20 * np.log10(np.abs(X.imag)), self.ui.bottom_f) else: X_r = np.maximum(20 * np.log10(np.abs(X)), self.ui.bottom_f) if plt_stimulus_q: Pxq = 10*np.log10(Pxq) if en_re_im_f: X_q_r = np.maximum(20 * np.log10(np.abs(X_q.real)), self.ui.bottom_f) X_q_i = np.maximum(20 * np.log10(np.abs(X_q.imag)), self.ui.bottom_f) else: X_q_r = np.maximum(20 * np.log10(np.abs(X_q)), self.ui.bottom_f) if plt_response: Py = 10*np.log10(Py) if en_re_im_f: Y_r = np.maximum(20 * np.log10(np.abs(Y.real)), self.ui.bottom_f) Y_i = np.maximum(20 * np.log10(np.abs(Y.imag)), self.ui.bottom_f) else: Y_r = np.maximum(20 * np.log10(np.abs(Y)), self.ui.bottom_f) if self.ui.chk_Hf.isChecked(): if en_re_im_f: H_id_r = np.maximum(20 * np.log10(np.abs(H_id.real)), self.ui.bottom_f) H_id_i = np.maximum(20 * np.log10(np.abs(H_id.imag)), self.ui.bottom_f) else: H_id_r = np.maximum(20 * np.log10(np.abs(H_id)), self.ui.bottom_f) else: H_F_pre = "" H_F_post = "" if plt_stimulus: if en_re_im_f: X_r = X.real X_i = X.imag else: X_r = np.abs(X) if plt_stimulus_q: if en_re_im_f: X_q_r = X_q.real X_q_i = X_q.imag else: X_q_r = np.abs(X_q) if plt_response: if en_re_im_f: Y_r = Y.real Y_i = Y.imag else: Y_r = np.abs(Y) if self.ui.chk_Hf.isChecked(): if en_re_im_f: H_id_r = H_id.real H_id_i = H_id.imag else: H_id_r = np.abs(H_id) unit = " in V" unit_P = "W" unit_nenbw = "bins" unit_cgain = "" nenbw = self.ui.nenbw cgain = self.ui.cgain if en_re_im_f: H_Fi_str = r'$\Im\{$' + H_F_str + r'$\}$' H_Fr_str = r'$\Re\{$' + H_F_str + r'$\}$' else: H_F_pre = "|" H_Fr_str = H_F_str H_Fi_str = 'undefined' H_F_post = "|" H_Fi_str = H_F_pre + H_Fi_str + H_F_post + unit H_Fr_str = H_F_pre + H_Fr_str + H_F_post + unit # ----------------------------------------------------------------- # --------------- Plot stimulus and response ---------------------- #------------------------------------------------------------------ show_info = self.ui.chk_show_info_freq.isChecked() if plt_stimulus: label_re = "|$X$" + ejO_str + "|" if en_re_im_f: label_re = "$X_r$" + ejO_str label_im = "$X_i$" + ejO_str self.draw_data(self.plt_freq_stim, self.ax_f2, F, X_i, label=label_im, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_stim, mkr=self.plt_freq_stim_mkr, mkr_fmt=self.fmt_mkr_stim) if show_info: label_re += ":\t$P$ = {0:.3g} {1}".format(Px, unit_P) self.draw_data(self.plt_freq_stim, self.ax_f1, F, X_r, label=label_re, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_stim, mkr=self.plt_freq_stim_mkr, mkr_fmt=self.fmt_mkr_stim) if plt_stimulus_q: label_re = "$|X_Q$" + ejO_str + "|" if en_re_im_f: label_re = "$X_{Q,r}$" + ejO_str label_im = "$X_{Q,i}$" + ejO_str self.draw_data(self.plt_freq_stmq, self.ax_f2, F, X_q_i, label=label_im, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_stmq, mkr=self.plt_freq_stmq_mkr, mkr_fmt=self.fmt_mkr_stmq) if show_info: label_re += ":\t$P$ = {0:.3g} {1}".format(Pxq, unit_P) self.draw_data(self.plt_freq_stmq, self.ax_f1, F, X_q_r, label=label_re, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_stmq, mkr=self.plt_freq_stmq_mkr, mkr_fmt=self.fmt_mkr_stmq) if plt_response: label_re = "$|Y$" + ejO_str + "|" if en_re_im_f: label_re = "$Y_r$" + ejO_str label_im = "$Y_i$" + ejO_str self.draw_data(self.plt_freq_resp, self.ax_f2, F, Y_i, label=label_im, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_resp, mkr=self.plt_freq_resp_mkr, mkr_fmt=self.fmt_mkr_resp) if show_info: label_re += ":\t$P$ = {0:.3g} {1}".format(Py, unit_P) self.draw_data(self.plt_freq_resp, self.ax_f1, F, Y_r, label=label_re, bottom=self.ui.bottom_f, plt_fmt=self.fmt_plot_resp, mkr=self.plt_freq_resp_mkr, mkr_fmt=self.fmt_mkr_resp) if self.ui.chk_Hf.isChecked(): label_re = "$|H_{id}$" + ejO_str + "|" if en_re_im_f: label_re = "$H_{id,r}$" + ejO_str label_im = "$H_{id,i}$" + ejO_str self.ax_f2.plot(F_id, H_id_i, c="gray",label=label_im) self.ax_f1.plot(F_id, H_id_r, c="gray",label=label_re) # --------------- LEGEND (real part) ---------------------------------- if self.plt_freq_enabled or self.ui.chk_Hf.isChecked(): self.ax_f1.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.7) # get handles and labels for all plots so far handles, labels = self.ax_f1.get_legend_handles_labels() # get a tuple with pairs of (label, handle), sorted for the label sorted_pairs = sorted(zip(labels, handles)) # convert back to two lists labels, handles = [ list(tuple) for tuple in zip(*sorted_pairs)] if show_info: # Create two empty patches for NENBW and CGAIN and extend handles list with them handles.extend([mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white", lw=0, alpha=0)] * 2) labels.append("$NENBW$:\t{0:.4g} {1}".format(nenbw, unit_nenbw)) labels.append("$CGAIN$:\t{0:.4g} {1}".format(cgain, unit_cgain)) self.ax_f1.legend(handles, labels, loc='best', fontsize='small', fancybox=True, framealpha=0.7) if en_re_im_f and self.plt_freq_enabled: self.ax_f2.legend(loc='best', fontsize='small', fancybox=True, framealpha=0.7) self.ax_f2.set_ylabel(H_Fi_str) self.axes_f[-1].set_xlabel(fb.fil[0]['plt_fLabel']) self.ax_f1.set_ylabel(H_Fr_str) #self.ax_f1.set_xlim(fb.fil[0]['freqSpecsRange']) self.ax_f1.set_xlim(F_range) self.ax_f1.set_title("Spectrum of " + 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_f1.get_ylim() self.ax_f1_noise.set_ylim(mn+corr, mx+corr) self.ax_f1_noise.set_ylabel(r'$P_N$ in dBW') self.redraw() # redraw currently active mplwidget self.needs_redraw[1] = False
#------------------------------------------------------------------------------
[docs] 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
# self.mplwidget_t.redraw() # self.mplwidget_f.redraw() #------------------------------------------------------------------------------ def main(): import sys from pyfda.libs.compat import QApplication app = QApplication(sys.argv) mainw = Plot_Impz(None) app.setActiveWindow(mainw) mainw.show() sys.exit(app.exec_()) if __name__ == "__main__": main() # module test using python -m pyfda.plot_widgets.plot_impz