class XStream(QtCore.QObject): """ subclass for log messages on logger window Overrides stdout to print messages to textWidget """ _stdout = None messageWritten = pyqtSignal(str) # pass str to slot def flush(self): pass def fileno(self): return -1 def write(self, msg): if not self.signalsBlocked(): msg = to_html(msg, frmt='log') self.messageWritten.emit(msg) @staticmethod def stdout(): if not XStream._stdout: XStream._stdout = XStream() sys.stdout = XStream._stdout return XStream._stdout
class Input_Fixpoint_Specs(QWidget): """ Create the widget that holds the dynamically loaded fixpoint filter ui """ # sig_resize = pyqtSignal() # emit a signal when the image has been resized sig_rx_local = pyqtSignal(object) # incoming from subwidgets -> process_sig_rx_local sig_rx = pyqtSignal(object) # incoming, connected to input_tab_widget.sig_rx sig_tx = pyqtSignal(object) # outcgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(Input_Fixpoint_Specs, self).__init__(parent) self.tab_label = 'Fixpoint' self.tool_tip = ("<span>Select a fixpoint implementation for the filter," " simulate it or generate a Verilog netlist.</span>") self.parent = parent self.fx_path = os.path.realpath( os.path.join(dirs.INSTALL_DIR, 'fixpoint_widgets')) self.no_fx_filter_img = os.path.join(self.fx_path, "no_fx_filter.png") if not os.path.isfile(self.no_fx_filter_img): logger.error("Image {0:s} not found!".format(self.no_fx_filter_img)) self.default_fx_img = os.path.join(self.fx_path, "default_fx_img.png") if not os.path.isfile(self.default_fx_img): logger.error("Image {0:s} not found!".format(self.default_fx_img)) self._construct_UI() inst_wdg_list = self._update_filter_cmb() if len(inst_wdg_list) == 0: logger.warning("No fixpoint filter found for this type of filter!") else: logger.debug("Imported {0:d} fixpoint filters:\n{1}" .format(len(inst_wdg_list.split("\n"))-1, inst_wdg_list)) self._update_fixp_widget() # ------------------------------------------------------------------------------ def process_sig_rx_local(self, dict_sig: dict = None) -> None: """ Process signals coming in from input and output quantizer subwidget and the dynamically instantiated subwidget and emit {'fx_sim': 'specs_changed'} in the end. """ if dict_sig['id'] == id(self): logger.warning(f'RX_LOCAL - Stopped infinite loop: "{first_item(dict_sig)}"') return elif 'fx_sim' in dict_sig and dict_sig['fx_sim'] == 'specs_changed': self.wdg_dict2ui() # update wordlengths in UI and set RUN button to 'changed' dict_sig.update({'id': id(self)}) # propagate 'specs_changed' with self 'id' self.emit(dict_sig) return # ---- Process input and output quantizer settings ('ui' in dict_sig) -- elif 'ui' in dict_sig: if 'wdg_name' not in dict_sig: logger.warning(f"No key 'wdg_name' in dict_sig:\n{pprint_log(dict_sig)}") return elif dict_sig['wdg_name'] == 'w_input': """ Input fixpoint format has been changed or butLock has been clicked. When I/O lock is active, copy input fixpoint word format to output word format. """ if dict_sig['ui'] == 'butLock'\ and not self.wdg_w_input.butLock.isChecked(): # butLock was deactivitated, don't do anything return elif self.wdg_w_input.butLock.isChecked(): # but lock was activated or wordlength setting have been changed fb.fil[0]['fxqc']['QO']['WI'] = fb.fil[0]['fxqc']['QI']['WI'] fb.fil[0]['fxqc']['QO']['WF'] = fb.fil[0]['fxqc']['QI']['WF'] fb.fil[0]['fxqc']['QO']['W'] = fb.fil[0]['fxqc']['QI']['W'] elif dict_sig['wdg_name'] == 'w_output': """ Output fixpoint format has been changed. When I/O lock is active, copy output fixpoint word format to input word format. """ if self.wdg_w_input.butLock.isChecked(): fb.fil[0]['fxqc']['QI']['WI'] = fb.fil[0]['fxqc']['QO']['WI'] fb.fil[0]['fxqc']['QI']['WF'] = fb.fil[0]['fxqc']['QO']['WF'] fb.fil[0]['fxqc']['QI']['W'] = fb.fil[0]['fxqc']['QO']['W'] elif dict_sig['wdg_name'] in {'q_output', 'q_input'}: pass else: logger.error("Unknown wdg_name '{0}' in dict_sig:\n{1}" .format(dict_sig['wdg_name'], pprint_log(dict_sig))) return if dict_sig['ui'] not in {'WI', 'WF', 'ovfl', 'quant', 'cmbW', 'butLock'}: logger.warning("Unknown value '{0}' for key 'ui'".format(dict_sig['ui'])) self.wdg_dict2ui() # update wordlengths in UI and set RUN button to 'changed' self.emit({'fx_sim': 'specs_changed'}) # propagate 'specs_changed' else: logger.error(f"Unknown key/value in 'dict_sig':\n{pprint_log(dict_sig)}") # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig: dict = None) -> None: """ Process signals coming in via `sig_rx` from other widgets. Trigger fx simulation: 1. ``fx_sim': 'init'``: Start fixpoint simulation by sending 'fx_sim':'start_fx_response_calculation' 2. ``fx_sim_calc_response()``: Receive stimulus from widget in 'fx_sim':'calc_frame_fx_response' and pass it to fixpoint simulation method 3. Store fixpoint response in `fb.fx_result` and return to initiating routine """ # logger.info( # "SIG_RX(): vis={0}\n{1}".format(self.isVisible(), pprint_log(dict_sig))) # logger.debug(f'SIG_RX(): "{first_item(dict_sig)}"') if dict_sig['id'] == id(self): # logger.warning(f'Stopped infinite loop: "{first_item(dict_sig)}"') return elif 'data_changed' in dict_sig and dict_sig['data_changed'] == "filter_designed": # New filter has been designed, update list of available filter topologies self._update_filter_cmb() return elif 'data_changed' in dict_sig or\ ('view_changed' in dict_sig and dict_sig['view_changed'] == 'q_coeff'): # Filter data has changed (but not the filter type) or the coefficient # format / wordlength have been changed in `input_coeffs`. The latter means # the view / display has been changed (wordlength) but not the actual # coefficients in the `input_coeffs` widget. However, the wordlength setting # is copied to the fxqc dict and from there to the fixpoint widget. # - update fields in the fixpoint filter widget - wordlength may have # been changed. # - Set RUN button to "changed" in wdg_dict2ui() self.wdg_dict2ui() # --------------- FX Simulation ------------------------------------------- elif 'fx_sim' in dict_sig: if dict_sig['fx_sim'] == 'init': # fixpoint simulation has been started externally, e.g. by # `impz.impz_init()`, return a handle to the fixpoint filter function # via signal-slot connection if not self.fx_wdg_found: logger.error("No fixpoint widget found!") qstyle_widget(self.butSimFx, "error") self.emit({'fx_sim': 'error'}) elif self.fx_sim_init() != 0: # returned an error qstyle_widget(self.butSimFx, "error") self.emit({'fx_sim': 'error'}) else: self.emit({'fx_sim': 'start_fx_response_calculation', 'fxfilter_func': self.fx_filt_ui.fxfilter}) elif dict_sig['fx_sim'] == 'calc_frame_fx_response': self.fx_sim_calc_response(dict_sig) # return to the routine collecting the response frame by frame return elif dict_sig['fx_sim'] == 'specs_changed': # fixpoint specification have been changed somewhere, update ui # and set run button to "changed" in wdg_dict2ui() self.wdg_dict2ui() elif dict_sig['fx_sim'] == 'finish': qstyle_widget(self.butSimFx, "normal") else: logger.error('Unknown "fx_sim" command option "{0}"\n' '\treceived from "{1}".' .format(dict_sig['fx_sim'], dict_sig['class'])) # ---- resize image when "Fixpoint" tab is selected or widget size is changed: elif 'ui_changed' in dict_sig and dict_sig['ui_changed'] in {'resized', 'tab'}\ and self.isVisible(): self.resize_img() # ------------------------------------------------------------------------------ def _construct_UI(self) -> None: """ Intitialize the main GUI, consisting of: - A combo box to select the filter topology and an image of the topology - The input quantizer - The UI of the fixpoint filter widget - Simulation and export buttons """ # ------------------------------------------------------------------------------ # Define frame and layout for the dynamically updated filter widget # The actual filter widget is instantiated in self.set_fixp_widget() later on self.layH_fx_wdg = QHBoxLayout() # self.layH_fx_wdg.setContentsMargins(*params['wdg_margins']) frmHDL_wdg = QFrame(self) frmHDL_wdg.setLayout(self.layH_fx_wdg) # frmHDL_wdg.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) # ------------------------------------------------------------------------------ # Initialize fixpoint filter combobox, title and description # ------------------------------------------------------------------------------ self.cmb_fx_wdg = QComboBox(self) self.cmb_fx_wdg.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.lblTitle = QLabel("not set", self) self.lblTitle.setWordWrap(True) self.lblTitle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) layHTitle = QHBoxLayout() layHTitle.addWidget(self.cmb_fx_wdg) layHTitle.addWidget(self.lblTitle) self.frmTitle = QFrame(self) self.frmTitle.setLayout(layHTitle) self.frmTitle.setContentsMargins(*params['wdg_margins']) # ------------------------------------------------------------------------------ # Input and Output Quantizer # ------------------------------------------------------------------------------ # - instantiate widgets for input and output quantizer # - pass the quantization (sub-?) dictionary to the constructor # ------------------------------------------------------------------------------ self.wdg_w_input = UI_W(self, q_dict=fb.fil[0]['fxqc']['QI'], wdg_name='w_input', label='', lock_visible=True) self.wdg_w_input.sig_tx.connect(self.process_sig_rx_local) cmb_q = ['round', 'floor', 'fix'] self.wdg_w_output = UI_W(self, q_dict=fb.fil[0]['fxqc']['QO'], wdg_name='w_output', label='') self.wdg_w_output.sig_tx.connect(self.process_sig_rx_local) self.wdg_q_output = UI_Q(self, q_dict=fb.fil[0]['fxqc']['QO'], wdg_name='q_output', label='Output Format <i>Q<sub>Y </sub></i>:', cmb_q=cmb_q, cmb_ov=['wrap', 'sat']) self.wdg_q_output.sig_tx.connect(self.sig_rx_local) if HAS_DS: cmb_q.append('dsm') self.wdg_q_input = UI_Q(self, q_dict=fb.fil[0]['fxqc']['QI'], wdg_name='q_input', label='Input Format <i>Q<sub>X </sub></i>:', cmb_q=cmb_q) self.wdg_q_input.sig_tx.connect(self.sig_rx_local) # Layout and frame for input quantization layVQiWdg = QVBoxLayout() layVQiWdg.addWidget(self.wdg_q_input) layVQiWdg.addWidget(self.wdg_w_input) frmQiWdg = QFrame(self) # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) frmQiWdg.setLayout(layVQiWdg) frmQiWdg.setContentsMargins(*params['wdg_margins']) # Layout and frame for output quantization layVQoWdg = QVBoxLayout() layVQoWdg.addWidget(self.wdg_q_output) layVQoWdg.addWidget(self.wdg_w_output) frmQoWdg = QFrame(self) # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) frmQoWdg.setLayout(layVQoWdg) frmQoWdg.setContentsMargins(*params['wdg_margins']) # ------------------------------------------------------------------------------ # Dynamically updated image of filter topology (label as placeholder) # ------------------------------------------------------------------------------ # allow setting background color # lbl_fixp_img_palette = QPalette() # lbl_fixp_img_palette.setColor(QPalette(window, Qt: white)) # lbl_fixp_img_palette.setBrush(self.backgroundRole(), QColor(150, 0, 0)) # lbl_fixp_img_palette.setColor(QPalette: WindowText, Qt: blue) self.lbl_fixp_img = QLabel("img not set", self) self.lbl_fixp_img.setAutoFillBackground(True) # self.lbl_fixp_img.setPalette(lbl_fixp_img_palette) # self.lbl_fixp_img.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.embed_fixp_img(self.no_fx_filter_img) layHImg = QHBoxLayout() layHImg.setContentsMargins(0, 0, 0, 0) layHImg.addWidget(self.lbl_fixp_img) # , Qt.AlignCenter) self.frmImg = QFrame(self) self.frmImg.setLayout(layHImg) self.frmImg.setContentsMargins(*params['wdg_margins']) # ------------------------------------------------------------------------------ # Simulation and export Buttons # ------------------------------------------------------------------------------ self.butExportHDL = QPushButton(self) self.butExportHDL.setToolTip( "Create Verilog or VHDL netlist for fixpoint filter.") self.butExportHDL.setText("Create HDL") self.butSimFx = QPushButton(self) self.butSimFx.setToolTip("Start fixpoint simulation.") self.butSimFx.setText("Sim. FX") self.layHHdlBtns = QHBoxLayout() self.layHHdlBtns.addWidget(self.butSimFx) self.layHHdlBtns.addWidget(self.butExportHDL) # This frame encompasses the HDL buttons sim and convert frmHdlBtns = QFrame(self) # frmBtns.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) frmHdlBtns.setLayout(self.layHHdlBtns) frmHdlBtns.setContentsMargins(*params['wdg_margins']) # ------------------------------------------------------------------- # Top level layout # ------------------------------------------------------------------- splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(frmHDL_wdg) splitter.addWidget(frmQoWdg) splitter.addWidget(self.frmImg) # 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, 3000, 5000]) layVMain = QVBoxLayout() layVMain.addWidget(self.frmTitle) layVMain.addWidget(frmHdlBtns) layVMain.addWidget(frmQiWdg) layVMain.addWidget(splitter) layVMain.addStretch() layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) self.sig_rx_local.connect(self.process_sig_rx_local) # dynamic connection in `self._update_fixp_widget()`: # ----- # if hasattr(self.fx_filt_ui, "sig_rx"): # self.sig_rx.connect(self.fx_filt_ui.sig_rx) # if hasattr(self.fx_filt_ui, "sig_tx"): # self.fx_filt_ui.sig_tx.connect(self.sig_rx_local) # ---- # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.cmb_fx_wdg.currentIndexChanged.connect(self._update_fixp_widget) self.butExportHDL.clicked.connect(self.exportHDL) self.butSimFx.clicked.connect(lambda x: self.emit({'fx_sim': 'start'})) # ---------------------------------------------------------------------- # EVENT FILTER # ---------------------------------------------------------------------- # # monitor events and generate sig_resize event when resized # self.lbl_fixp_img.installEventFilter(self) # # ... then redraw image when resized # self.sig_resize.connect(self.resize_img) # ------------------------------------------------------------------------------ def _update_filter_cmb(self) -> str: """ (Re-)Read list of available fixpoint filters for a given filter design every time a new filter design is selected. Then try to import the fixpoint designs in the list and populate the fixpoint implementation combo box `self.cmb_fx_wdg` when successfull. Returns ------- inst_wdg_str: str string with all fixpoint widgets that could be instantiated successfully """ inst_wdg_str = "" # full names of successfully instantiated widgets for logging # remember last fx widget setting: last_fx_wdg = qget_cmb_box(self.cmb_fx_wdg, data=False) self.cmb_fx_wdg.clear() fc = fb.fil[0]['fc'] if 'fix' in fb.filter_classes[fc]: self.cmb_fx_wdg.blockSignals(True) for class_name in fb.filter_classes[fc]['fix']: # get class name try: # construct module + class name ... mod_class_name = fb.fixpoint_classes[class_name]['mod'] + '.'\ + class_name # ... and display name disp_name = fb.fixpoint_classes[class_name]['name'] self.cmb_fx_wdg.addItem(disp_name, mod_class_name) inst_wdg_str += '\t' + class_name + ' : ' + mod_class_name + '\n' except AttributeError as e: logger.warning('Widget "{0}":\n{1}'.format(class_name, e)) self.embed_fixp_img(self.no_fx_filter_img) continue # with next `class_name` of for loop except KeyError as e: logger.warning("No fixpoint filter for filter type {0} available." .format(e)) self.embed_fixp_img(self.no_fx_filter_img) continue # with next `class_name` of for loop # restore last fx widget if possible idx = self.cmb_fx_wdg.findText(last_fx_wdg) # set to idx 0 if not found (returned -1) self.cmb_fx_wdg.setCurrentIndex(max(idx, 0)) self.cmb_fx_wdg.blockSignals(False) else: # no fixpoint widget self.embed_fixp_img(self.no_fx_filter_img) self._update_fixp_widget() return inst_wdg_str # # ------------------------------------------------------------------------------ # def eventFilter(self, source, event): # """ # Filter all events generated by monitored QLabel, only resize events are # processed here, generating a `sig_resize` signal. All other events # are passed on to the next hierarchy level. # """ # if event.type() == QEvent.Resize: # logger.warning("resize event!") # self.sig_resize.emit() # # Call base class method to continue normal event processing: # return super(Input_Fixpoint_Specs, self).eventFilter(source, event) # ------------------------------------------------------------------------------ def embed_fixp_img(self, img_file: str) -> QPixmap: """ Embed `img_file` in png format as `self.img_fixp` Parameters ---------- img_file: str path and file name to image file Returns ------- self.img_fixp: QPixmap object pixmap containing the passed img_file """ if not os.path.isfile(img_file): logger.warning("Image file {0} doesn't exist.".format(img_file)) img_file = self.default_fx_img _, file_extension = os.path.splitext(img_file) if file_extension != '.png': logger.error('Unknown file extension "{0}"!'.format(file_extension)) img_file = self.default_fx_img self.img_fixp = QPixmap(img_file) # logger.warning(f"img_fixp = {img_file}") # logger.warning(f"_embed_fixp_img(): {self.img_fixp.__class__.__name__}") return self.img_fixp # ------------------------------------------------------------------------------ def resize_img(self) -> None: """ Triggered when `self` (the widget) is selected or resized. The method resizes the image inside QLabel to completely fill the label while keeping the aspect ratio. An offset of some pixels is needed, otherwise the image is clipped. """ # logger.warning(f"resize_img(): img_fixp = {self.img_fixp.__class__.__name__}") if self.parent is None: # parent is QApplication, has no width or height par_w, par_h = 300, 700 # fixed size for module level test else: # widget parent is InputTabWidget() par_w, par_h = self.parent.width(), self.parent.height() img_w, img_h = self.img_fixp.width(), self.img_fixp.height() if img_w > 10: max_h = int(max(np.floor(img_h * par_w/img_w) - 5, 20)) else: max_h = 200 logger.debug("img size: {0},{1}, frm size: {2},{3}, max_h: {4}" .format(img_w, img_h, par_w, par_h, max_h)) # The following doesn't work because the width of the parent widget can grow # with the image size # img_scaled = self.img_fixp.scaled(self.lbl_fixp_img.size(), # Qt.KeepAspectRatio, Qt.SmoothTransformation) img_scaled = self.img_fixp.scaledToHeight(max_h, Qt.SmoothTransformation) self.lbl_fixp_img.setPixmap(img_scaled) # ------------------------------------------------------------------------------ def _update_fixp_widget(self): """ This method is called at the initialization of the widget and when a new fixpoint filter implementation is selected from the combo box: - Destruct old instance of fixpoint filter widget `self.fx_filt_ui` - Import and instantiate new fixpoint filter widget e.g. after changing the filter topology as - Try to load image for filter topology - Update the UI of the widget - Try to instantiate HDL filter as `self.fx_filt_ui.fixp_filter` with dummy data - emit {'fx_sim': 'specs_changed'} when successful """ def _disable_fx_wdg(self) -> None: if hasattr(self, "fx_filt_ui") and self.fx_filt_ui is not None: # is a fixpoint widget loaded? try: # try to remove widget from layout self.layH_fx_wdg.removeWidget(self.fx_filt_ui) # delete QWidget when scope has been left self.fx_filt_ui.deleteLater() except AttributeError as e: logger.error("Destructing UI failed!\n{0}".format(e)) self.fx_wdg_found = False self.butSimFx.setEnabled(False) self.butExportHDL.setVisible(False) # self.layH_fx_wdg.setVisible(False) self.img_fixp = self.embed_fixp_img(self.no_fx_filter_img) self.resize_img() self.lblTitle.setText("") self.fx_filt_ui = None # ----------------------------------------------------------- _disable_fx_wdg(self) # destruct old fixpoint widget instance: # instantiate new fixpoint widget class as self.fx_filt_ui cmb_wdg_fx_cur = qget_cmb_box(self.cmb_fx_wdg, data=False) if cmb_wdg_fx_cur: # at least one valid fixpoint widget found self.fx_wdg_found = True # get list [module name and path, class name] fx_mod_class_name = qget_cmb_box(self.cmb_fx_wdg, data=True).rsplit('.', 1) fx_mod = importlib.import_module(fx_mod_class_name[0]) # get module fx_filt_ui_class = getattr(fx_mod, fx_mod_class_name[1]) # get class logger.info("Instantiating new FX widget\n\t" f"{fx_mod.__name__}.{fx_filt_ui_class.__name__}") # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ self.fx_filt_ui = fx_filt_ui_class() # instantiate the fixpoint widget # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # and add it to layout: self.layH_fx_wdg.addWidget(self.fx_filt_ui, stretch=1) self.fx_filt_ui.setVisible(True) self.wdg_dict2ui() # initialize the fixpoint subwidgets from the fxqc_dict # ---- connect signals to fx_filt_ui ---- if hasattr(self.fx_filt_ui, "sig_rx"): self.sig_rx.connect(self.fx_filt_ui.sig_rx) if hasattr(self.fx_filt_ui, "sig_tx"): self.fx_filt_ui.sig_tx.connect(self.sig_rx_local) # ---- get name of new fixpoint filter image ---- if not (hasattr(self.fx_filt_ui, "img_name") and self.fx_filt_ui.img_name): # no image name defined, use default image img_file = self.default_fx_img else: # get path of imported fixpoint widget ... file_path = os.path.dirname(fx_mod.__file__) # ... and construct full image name from it img_file = os.path.join(file_path, self.fx_filt_ui.img_name) # ---- instantiate and scale graphic of filter topology ---- self.embed_fixp_img(img_file) self.resize_img() # ---- set title and description for filter self.lblTitle.setText(self.fx_filt_ui.title) # Check which methods the fixpoint widget provides and enable # corresponding buttons: self.butExportHDL.setVisible(hasattr(self.fx_filt_ui, "to_hdl")) self.butSimFx.setEnabled(hasattr(self.fx_filt_ui, "fxfilter")) self.update_fxqc_dict() self.emit({'fx_sim': 'specs_changed'}) # ------------------------------------------------------------------------------ def wdg_dict2ui(self): """ Trigger an update of the fixpoint widget UI when view (i.e. fixpoint coefficient format) or data have been changed outside this class. Additionally, pass the fixpoint quantization widget to update / restore other subwidget settings. Set the RUN button to "changed". """ # fb.fil[0]['fxqc']['QCB'].update({'scale':(1 << fb.fil[0]['fxqc']['QCB']['W'])}) self.wdg_q_input.dict2ui(fb.fil[0]['fxqc']['QI']) self.wdg_q_output.dict2ui(fb.fil[0]['fxqc']['QO']) self.wdg_w_input.dict2ui(fb.fil[0]['fxqc']['QI']) self.wdg_w_output.dict2ui(fb.fil[0]['fxqc']['QO']) if self.fx_wdg_found and hasattr(self.fx_filt_ui, "dict2ui"): self.fx_filt_ui.dict2ui() # dict_sig = {'fx_sim':'specs_changed'} # self.emit(dict_sig) qstyle_widget(self.butSimFx, "changed") # ------------------------------------------------------------------------------ def update_fxqc_dict(self): """ Update the fxqc dictionary before simulation / HDL generation starts. """ if self.fx_wdg_found: # get a dict with the coefficients and fixpoint settings from fixpoint widget if hasattr(self.fx_filt_ui, "ui2dict"): fb.fil[0]['fxqc'].update(self.fx_filt_ui.ui2dict()) logger.debug("update fxqc: \n{0}".format(pprint_log(fb.fil[0]['fxqc']))) else: logger.error("No fixpoint widget found!") # ------------------------------------------------------------------------------ def exportHDL(self): """ Synthesize HDL description of filter """ dlg = QFD(self) # instantiate file dialog object file_types = "Verilog (*.v)" # needed for overwrite confirmation when name is entered without suffix: dlg.setDefaultSuffix('v') dlg.setWindowTitle('Export Verilog') dlg.setNameFilter(file_types) dlg.setDirectory(dirs.save_dir) # set mode "save file" instead "open file": dlg.setAcceptMode(QFD.AcceptSave) dlg.setOption(QFD.DontConfirmOverwrite, False) if dlg.exec_() == QFD.Accepted: hdl_file = qstr(dlg.selectedFiles()[0]) # hdl_type = extract_file_ext(qstr(dlg.selectedNameFilter()))[0] # ============================================================================= # # static method getSaveFileName_() is simple but unflexible # hdl_file, hdl_filter = dlg.getSaveFileName_( # caption="Save Verilog netlist as (this also defines the module name)", # directory=dirs.save_dir, filter=file_types) # hdl_file = qstr(hdl_file) # if hdl_file != "": # "operation cancelled" returns an empty string # # return '.v' or '.vhd' depending on filetype selection: # # hdl_type = extract_file_ext(qstr(hdl_filter))[0] # # sanitized dir + filename + suffix. The filename suffix is replaced # # by `v` later. # hdl_file = os.path.normpath(hdl_file) # complete path + file name # ============================================================================= hdl_dir_name = os.path.dirname(hdl_file) # extract the directory path if not os.path.isdir(hdl_dir_name): # create directory if it doesn't exist os.mkdir(hdl_dir_name) dirs.save_dir = hdl_dir_name # make this directory the new default / base dir hdl_file_name = os.path.splitext(os.path.basename(hdl_file))[0] hdl_full_name = os.path.join(hdl_dir_name, hdl_file_name + ".v") # remove all non-alphanumeric chars: vlog_mod_name = re.sub(r'\W+', '', hdl_file_name).lower() logger.info('Creating hdl_file "{0}"\n\twith top level module "{1}"' .format(hdl_full_name, vlog_mod_name)) try: self.update_fxqc_dict() self.fx_filt_ui.construct_fixp_filter() code = self.fx_filt_ui.to_hdl(name=vlog_mod_name) # logger.info(str(code)) # print verilog code to console with io.open(hdl_full_name, 'w', encoding="utf8") as f: f.write(str(code)) logger.info("HDL conversion finished!") except (IOError, TypeError) as e: logger.warning(e) # -------------------------------------------------------------------------- def fx_sim_init(self): """ Initialize fix-point simulation: - Update the `fxqc_dict` containing all quantization information - Setup a filter instance for fixpoint simulation - Request a stimulus signal Returns ------- error: int 0 for sucessful fx widget construction, -1 for error """ try: self.update_fxqc_dict() self.fx_filt_ui.init_filter() # setup filter instance return 0 except ValueError as e: logger.error('Fixpoint stimulus generation failed during "init"' '\nwith "{0} "'.format(e)) return -1 # ------------------------------------------------------------------------------ def fx_sim_calc_response(self, dict_sig) -> None: """ - Read fixpoint stimulus from `dict_sig` in integer format - Pass it to the fixpoint filter which calculates the fixpoint response - Store the result in `fb.fx_results` and return. In case of an error, `fb.fx_results == None` Returns ------- None """ try: # logger.info( # 'Simulate fixpoint frame with "{0}" stimulus:\n\t{1}'.format( # dict_sig['class'], # pprint_log(dict_sig['fx_stimulus'], tab=" "), # )) # Run fixpoint simulation and store the results as integer values: fb.fx_results = self.fx_filt_ui.fxfilter(dict_sig['fx_stimulus']) if len(fb.fx_results) == 0: logger.error("Fixpoint simulation returned empty results!") # else: # # logger.debug("fx_results: {0}"\ # # .format(pprint_log(fb.fx_results, tab= " "))) # logger.info( # f'Fixpoint simulation successful for dict\n{pprint_log(dict_sig)}' # f'\tStimuli: Shape {np.shape(dict_sig["fx_stimulus"])}' # f' of type "{dict_sig["fx_stimulus"].dtype}"' # f'\n\tResponse: Shape {np.shape(fb.fx_results)}' # f' of type "{type(fb.fx_results).__name__} "' # f' ("{type(fb.fx_results[0]).__name__}")' # ) except ValueError as e: logger.error("Simulator error {0}".format(e)) fb.fx_results = None except AssertionError as e: logger.error('Fixpoint simulation failed for dict\n{0}' '\twith msg. "{1}"\n\tStimuli: Shape {2} of type "{3}"' '\n\tResponse: Shape {4} of type "{5}"'.format( pprint_log(dict_sig), e, np.shape(dict_sig['fx_stimulus']), dict_sig['fx_stimulus'].dtype, np.shape(fb.fx_results), type(fb.fx_results) )) fb.fx_results = None if fb.fx_results is None: qstyle_widget(self.butSimFx, "error") else: pass # everything ok, return # logger.debug("Sending fixpoint results") return
class Input_Info(QWidget): """ Create widget for displaying infos about filter specs and filter design method """ sig_rx = pyqtSignal(object) # incoming signals from input_tab_widgets sig_tx = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(Input_Info, self).__init__(parent) self.tab_label = 'Info' self.tool_tip = ( "<span>Display the achieved filter specifications" " and more info about the filter design algorithm.</span>") self._construct_UI() self.load_dict() def process_sig_rx(self, dict_sig=None): """ Process signals coming from sig_rx """ # logger.debug("Processing {0}: {1}".format(type(dict_sig).__name__, dict_sig)) if 'data_changed' in dict_sig or 'view_changed' in dict_sig\ or 'specs_changed' in dict_sig: self.load_dict() def _construct_UI(self): """ Intitialize the widget, consisting of: - Checkboxes for selecting the info to be displayed - A large text window for displaying infos about the filter design algorithm """ bfont = QFont() bfont.setBold(True) # ============== UI Layout ===================================== # widget / subwindow for filter infos # self.butFiltPerf = QToolButton("H(f)", self) self.butFiltPerf = QPushButton(self) self.butFiltPerf.setText("H(f)") self.butFiltPerf.setCheckable(True) self.butFiltPerf.setChecked(True) self.butFiltPerf.setToolTip("Display frequency response at test frequencies.") self.butDebug = QPushButton(self) self.butDebug.setText("Debug") self.butDebug.setCheckable(True) self.butDebug.setChecked(False) self.butDebug.setToolTip("Show debugging options.") self.butAbout = QPushButton("About", self) # pop-up "About" window self.butSettings = QPushButton("Settings", self) # self.butSettings.setCheckable(True) self.butSettings.setChecked(False) self.butSettings.setToolTip("Display and set some settings") layHControls1 = QHBoxLayout() layHControls1.addWidget(self.butFiltPerf) layHControls1.addWidget(self.butAbout) layHControls1.addWidget(self.butSettings) layHControls1.addWidget(self.butDebug) self.butDocstring = QPushButton("Doc$", self) self.butDocstring.setCheckable(True) self.butDocstring.setChecked(False) self.butDocstring.setToolTip("Display docstring from python filter method.") self.butRichText = QPushButton("RTF", self) self.butRichText.setCheckable(HAS_DOCUTILS) self.butRichText.setChecked(HAS_DOCUTILS) self.butRichText.setEnabled(HAS_DOCUTILS) self.butRichText.setToolTip("Render documentation in Rich Text Format.") self.butFiltDict = QPushButton("FiltDict", self) self.butFiltDict.setToolTip("Show filter dictionary for debugging.") self.butFiltDict.setCheckable(True) self.butFiltDict.setChecked(False) self.butFiltTree = QPushButton("FiltTree", self) self.butFiltTree.setToolTip("Show filter tree for debugging.") self.butFiltTree.setCheckable(True) self.butFiltTree.setChecked(False) layHControls2 = QHBoxLayout() layHControls2.addWidget(self.butDocstring) # layHControls2.addStretch(1) layHControls2.addWidget(self.butRichText) # layHControls2.addStretch(1) layHControls2.addWidget(self.butFiltDict) # layHControls2.addStretch(1) layHControls2.addWidget(self.butFiltTree) self.frmControls2 = QFrame(self) self.frmControls2.setLayout(layHControls2) self.frmControls2.setVisible(self.butDebug.isChecked()) self.frmControls2.setContentsMargins(0, 0, 0, 0) lbl_settings_NFFT = QLabel(to_html("N_FFT =", frmt='bi'), self) self.led_settings_NFFT = QLineEdit(self) self.led_settings_NFFT.setText(str(params['N_FFT'])) self.led_settings_NFFT.setToolTip("<span>Number of FFT points for frequency " "domain widgets.</span>") layGSettings = QGridLayout() layGSettings.addWidget(lbl_settings_NFFT, 1, 0) layGSettings.addWidget(self.led_settings_NFFT, 1, 1) self.frmSettings = QFrame(self) self.frmSettings.setLayout(layGSettings) self.frmSettings.setVisible(self.butSettings.isChecked()) self.frmSettings.setContentsMargins(0, 0, 0, 0) layVControls = QVBoxLayout() layVControls.addLayout(layHControls1) layVControls.addWidget(self.frmControls2) layVControls.addWidget(self.frmSettings) self.frmMain = QFrame(self) self.frmMain.setLayout(layVControls) 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.txtFiltInfoBox = QTextBrowser(self) self.txtFiltDict = QTextBrowser(self) self.txtFiltTree = QTextBrowser(self) layVMain = QVBoxLayout() layVMain.addWidget(self.frmMain) # layVMain.addLayout(self.layHControls) splitter = QSplitter(self) splitter.setOrientation(Qt.Vertical) splitter.addWidget(self.tblFiltPerf) splitter.addWidget(self.txtFiltInfoBox) splitter.addWidget(self.txtFiltDict) splitter.addWidget(self.txtFiltTree) # 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, 10000, 1000, 1000]) layVMain.addWidget(splitter) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.butFiltPerf.clicked.connect(self._show_filt_perf) self.butAbout.clicked.connect(self._about_window) self.butSettings.clicked.connect(self._show_settings) self.led_settings_NFFT.editingFinished.connect(self._update_settings_nfft) self.butDebug.clicked.connect(self._show_debug) self.butFiltDict.clicked.connect(self._show_filt_dict) self.butFiltTree.clicked.connect(self._show_filt_tree) self.butDocstring.clicked.connect(self._show_doc) self.butRichText.clicked.connect(self._show_doc) def _about_window(self): self.about_widget = AboutWindow(self) # important: Handle must be class attribute # self.opt_widget.show() # modeless dialog, i.e. non-blocking self.about_widget.exec_() # modal dialog (blocking) # ------------------------------------------------------------------------------ def _show_debug(self): """ Show / hide debug options depending on the state of the debug button """ self.frmControls2.setVisible(self.butDebug.isChecked()) # ------------------------------------------------------------------------------ def _show_settings(self): """ Show / hide settings options depending on the state of the settings button """ self.frmSettings.setVisible(self.butSettings.isChecked()) def _update_settings_nfft(self): """ Update value for self.par1 from QLineEdit Widget""" params['N_FFT'] = safe_eval(self.led_settings_NFFT.text(), params['N_FFT'], sign='pos', return_type='int') self.led_settings_NFFT.setText(str(params['N_FFT'])) self.emit({'data_changed': 'n_fft'}) # ------------------------------------------------------------------------------ def load_dict(self): """ update docs and filter performance """ self._show_doc() self._show_filt_perf() self._show_filt_dict() self._show_filt_tree() # ------------------------------------------------------------------------------ def _show_doc(self): """ Display info from filter design file and docstring """ if hasattr(ff.fil_inst, 'info'): if self.butRichText.isChecked(): self.txtFiltInfoBox.setText(publish_string( self._clean_doc(ff.fil_inst.info), writer_name='html', settings_overrides={'output_encoding': 'unicode'})) else: self.txtFiltInfoBox.setText(textwrap.dedent(ff.fil_inst.info)) else: self.txtFiltInfoBox.setText("") if self.butDocstring.isChecked() and hasattr(ff.fil_inst, 'info_doc'): if self.butRichText.isChecked(): self.txtFiltInfoBox.append( '<hr /><b>Python module docstring:</b>\n') for doc in ff.fil_inst.info_doc: self.txtFiltInfoBox.append(publish_string( self._clean_doc(doc), writer_name='html', settings_overrides={'output_encoding': 'unicode'})) else: self.txtFiltInfoBox.append('\nPython module docstring:\n') for doc in ff.fil_inst.info_doc: self.txtFiltInfoBox.append(self._clean_doc(doc)) self.txtFiltInfoBox.moveCursor(QTextCursor.Start) def _clean_doc(self, doc): """ Remove uniform number of leading blanks from docstrings for subsequent processing of rich text. The first line is treated differently, _all_ leading blanks are removed (if any). This allows for different formats of docstrings. """ lines = doc.splitlines() result = lines[0].lstrip() + "\n" + textwrap.dedent("\n".join(lines[1:])) return result # ------------------------------------------------------------------------------ def _show_filt_perf(self): """ Print filter properties in a table at frequencies of interest. When specs are violated, colour the table entry in red. """ antiC = False def _find_min_max(self, f_start, f_stop, unit='dB'): """ Find minimum and maximum magnitude and the corresponding frequencies for the filter defined in the filter dict in a given frequency band [f_start, f_stop]. """ w = np.linspace(f_start, f_stop, params['N_FFT'])*2*np.pi [w, H] = sig.freqz(bb, aa, worN=w) # add antiCausals if we have them if (antiC): # # Evaluate transfer function of anticausal half on the same freq grid. # wa, ha = sig.freqz(bbA, aaA, worN=w) ha = ha.conjugate() # # Total transfer function is the product # H = H*ha f = w / (2.0 * pi) # frequency normalized to f_S H_abs = abs(H) H_max = max(H_abs) H_min = min(H_abs) F_max = f[np.argmax(H_abs)] # find the frequency where H_abs F_min = f[np.argmin(H_abs)] # becomes max resp. min if unit == 'dB': H_max = 20*log10(H_max) H_min = 20*log10(H_min) return F_min, H_min, F_max, H_max # ------------------------------------------------------------------ self.tblFiltPerf.setVisible(self.butFiltPerf.isChecked()) if self.butFiltPerf.isChecked(): bb = fb.fil[0]['ba'][0] aa = fb.fil[0]['ba'][1] # 'rpk' means nonCausal filter if 'rpk' in fb.fil[0]: antiC = True bbA = fb.fil[0]['baA'][0] aaA = fb.fil[0]['baA'][1] bbA = bbA.conjugate() aaA = aaA.conjugate() f_S = fb.fil[0]['f_S'] f_lbls = [] f_vals = [] a_lbls = [] a_targs = [] a_targs_dB = [] a_test = [] ft = fb.fil[0]['ft'] # get filter type ('IIR', 'FIR') unit = fb.fil[0]['amp_specs_unit'] unit = 'dB' # fix this for the moment # construct pairs of corner frequency and corresponding amplitude # labels in ascending frequency for each response type if fb.fil[0]['rt'] in {'LP', 'HP', 'BP', 'BS', 'HIL'}: if fb.fil[0]['rt'] == 'LP': f_lbls = ['F_PB', 'F_SB'] a_lbls = ['A_PB', 'A_SB'] elif fb.fil[0]['rt'] == 'HP': f_lbls = ['F_SB', 'F_PB'] a_lbls = ['A_SB', 'A_PB'] elif fb.fil[0]['rt'] == 'BP': f_lbls = ['F_SB', 'F_PB', 'F_PB2', 'F_SB2'] a_lbls = ['A_SB', 'A_PB', 'A_PB', 'A_SB2'] elif fb.fil[0]['rt'] == 'BS': f_lbls = ['F_PB', 'F_SB', 'F_SB2', 'F_PB2'] a_lbls = ['A_PB', 'A_SB', 'A_SB', 'A_PB2'] elif fb.fil[0]['rt'] == 'HIL': f_lbls = ['F_PB', 'F_PB2'] a_lbls = ['A_PB', 'A_PB'] # Try to get lists of frequency / amplitude specs from the filter dict # that correspond to the f_lbls / a_lbls pairs defined above # When one of the labels doesn't exist in the filter dict, delete # all corresponding amplitude and frequency entries. err = [False] * len(f_lbls) # initialize error list f_vals = [] a_targs = [] for i in range(len(f_lbls)): try: f = fb.fil[0][f_lbls[i]] f_vals.append(f) except KeyError as e: f_vals.append('') err[i] = True logger.debug(e) try: a = fb.fil[0][a_lbls[i]] a_dB = lin2unit(fb.fil[0][a_lbls[i]], ft, a_lbls[i], unit) a_targs.append(a) a_targs_dB.append(a_dB) except KeyError as e: a_targs.append('') a_targs_dB.append('') err[i] = True logger.debug(e) for i in range(len(f_lbls)): if err[i]: del f_lbls[i] del f_vals[i] del a_lbls[i] del a_targs[i] del a_targs_dB[i] f_vals = np.asarray(f_vals) # convert to numpy array logger.debug("F_test_labels = %s" % f_lbls) # Calculate frequency response at test frequencies [w_test, a_test] = sig.freqz(bb, aa, 2.0 * pi * f_vals.astype(float)) # add antiCausals if we have them if (antiC): wa, ha = sig.freqz(bbA, aaA, 2.0 * pi * f_vals.astype(float)) ha = ha.conjugate() a_test = a_test*ha (F_min, H_min, F_max, H_max) = _find_min_max(self, 0, 1, unit='V') # append frequencies and values for min. and max. filter reponse to # test vector f_lbls += ['Min.', 'Max.'] # QTableView does not support direct formatting, use QLabel f_vals = np.append(f_vals, [F_min, F_max]) a_targs = np.append(a_targs, [np.nan, np.nan]) a_targs_dB = np.append(a_targs_dB, [np.nan, np.nan]) a_test = np.append(a_test, [H_min, H_max]) # calculate response of test frequencies in dB a_test_dB = -20*log10(abs(a_test)) # get filter type ('IIR', 'FIR') for dB <-> lin conversion ft = fb.fil[0]['ft'] # unit = fb.fil[0]['amp_specs_unit'] unit = 'dB' # make this fixed for the moment # build a list with the corresponding target specs: a_targs_pass = [] eps = 1e-3 for i in range(len(f_lbls)): if 'PB' in f_lbls[i]: a_targs_pass.append((a_test_dB[i] - a_targs_dB[i]) < eps) a_test[i] = 1 - abs(a_test[i]) elif 'SB' in f_lbls[i]: a_targs_pass.append(a_test_dB[i] >= a_targs_dB[i]) else: a_targs_pass.append(True) self.targs_spec_passed = np.all(a_targs_pass) logger.debug( "H_targ = {0}\n" "H_test = {1}\n" "H_test_dB = {2}\n" "F_test = {3}\n" "H_targ_pass = {4}\n" "passed: {5}\n".format(a_targs, a_test, a_test_dB, f_vals, a_targs_pass, self.targs_spec_passed)) self.tblFiltPerf.setRowCount(len(a_test)) # number of table rows self.tblFiltPerf.setColumnCount(5) # number of table columns self.tblFiltPerf.setHorizontalHeaderLabels([ 'f/{0:s}'.format(fb.fil[0]['freq_specs_unit']), 'Spec\n(dB)', '|H(f)|\n(dB)', 'Spec', '|H(f)|']) self.tblFiltPerf.setVerticalHeaderLabels(f_lbls) for row in range(len(a_test)): self.tblFiltPerf.setItem( row, 0, QTableWidgetItem(str('{0:.4g}'.format(f_vals[row]*f_S)))) self.tblFiltPerf.setItem( row, 1, QTableWidgetItem(str('%2.3g'%(-a_targs_dB[row])))) self.tblFiltPerf.setItem( row, 2, QTableWidgetItem(str('%2.3f'%(-a_test_dB[row])))) if a_targs[row] < 0.01: self.tblFiltPerf.setItem( row, 3, QTableWidgetItem(str('%.3e'%(a_targs[row])))) else: self.tblFiltPerf.setItem( row, 3, QTableWidgetItem(str('%2.4f'%(a_targs[row])))) if a_test[row] < 0.01: self.tblFiltPerf.setItem( row, 4, QTableWidgetItem(str('%.3e'%(abs(a_test[row]))))) else: self.tblFiltPerf.setItem( row, 4, QTableWidgetItem(str('%.4f'%(abs(a_test[row]))))) if not a_targs_pass[row]: self.tblFiltPerf.item(row, 1).setBackground(QtGui.QColor('red')) self.tblFiltPerf.item(row, 3).setBackground(QtGui.QColor('red')) self.tblFiltPerf.resizeColumnsToContents() self.tblFiltPerf.resizeRowsToContents() # ------------------------------------------------------------------------------ def _show_filt_dict(self): """ Print filter dict for debugging """ self.txtFiltDict.setVisible(self.butFiltDict.isChecked()) fb_sorted = [str(key) + ' : ' + str(fb.fil[0][key]) for key in sorted(fb.fil[0].keys())] dictstr = pprint.pformat(fb_sorted) # dictstr = pprint.pformat(fb.fil[0]) self.txtFiltDict.setText(dictstr) # ------------------------------------------------------------------------------ def _show_filt_tree(self): """ Print filter tree for debugging """ self.txtFiltTree.setVisible(self.butFiltTree.isChecked()) ftree_sorted = ['<b>' + str(key) + ' : ' + '</b>' + str(fb.fil_tree[key]) for key in sorted(fb.fil_tree.keys())] dictstr = pprint.pformat(ftree_sorted, indent=4) # dictstr = pprint.pformat(fb.fil[0]) self.txtFiltTree.setText(dictstr)
class UI_Q(QWidget): """ Widget for selecting quantization / overflow options. The result can be read out via the attributes `self.ovfl` and `self.quant`. The constructor accepts a reference to the quantization dictionary for initial widget settings and for (re-)storing values. The following keys are defined; default values are used for missing keys: 'wdg_name' : 'ui_q' # widget name 'label' : '' # widget text label 'label_q' : 'Quant.' # subwidget text label 'tip_q' : 'Select kind of quantization.' # Mouse-over tooltip 'cmb_q' : [round', 'fix', 'floor'] # combo-box choices 'cur_q' : 'round' # initial / current setting 'label_ov' : 'Ovfl.' # subwidget text label 'tip_ov' : 'Select overflow behaviour.' # Mouse-over tooltip 'cmb_ov' : ['wrap', 'sat'] # combo-box choices 'cur_ov' : 'wrap' # initial / current setting 'enabled' : True # Is widget enabled? 'visible' : True # Is widget visible? """ # incoming, # sig_rx = pyqtSignal(object) # outcgoing sig_tx = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent, q_dict, **kwargs): super(UI_Q, self).__init__(parent) self.q_dict = q_dict self._construct_UI(**kwargs) def _construct_UI(self, **kwargs): """ Construct widget """ dict_ui = { 'wdg_name': 'ui_q', 'label': '', 'label_q': 'Quant.', 'tip_q': 'Select the kind of quantization.', 'cmb_q': ['round', 'fix', 'floor'], 'cur_q': 'round', 'label_ov': 'Ovfl.', 'tip_ov': 'Select overflow behaviour.', 'cmb_ov': ['wrap', 'sat'], 'cur_ov': 'wrap', 'enabled': True, 'visible': True } #: default widget settings if 'quant' in self.q_dict and self.q_dict['quant'] in dict_ui['cmb_q']: dict_ui['cur_q'] = self.q_dict['quant'] if 'ovfl' in self.q_dict and self.q_dict['ovfl'] in dict_ui['cmb_ov']: dict_ui['cur_ov'] = self.q_dict['ovfl'] for key, val in kwargs.items(): dict_ui.update({key: val}) # dict_ui.update(map(kwargs)) # same as above? self.wdg_name = dict_ui['wdg_name'] lblQuant = QLabel(dict_ui['label_q'], self) self.cmbQuant = QComboBox(self) self.cmbQuant.addItems(dict_ui['cmb_q']) qset_cmb_box(self.cmbQuant, dict_ui['cur_q']) self.cmbQuant.setToolTip(dict_ui['tip_q']) self.cmbQuant.setObjectName('quant') lblOvfl = QLabel(dict_ui['label_ov'], self) self.cmbOvfl = QComboBox(self) self.cmbOvfl.addItems(dict_ui['cmb_ov']) qset_cmb_box(self.cmbOvfl, dict_ui['cur_ov']) self.cmbOvfl.setToolTip(dict_ui['tip_ov']) self.cmbOvfl.setObjectName('ovfl') # ComboBox size is adjusted automatically to fit the longest element self.cmbQuant.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbOvfl.setSizeAdjustPolicy(QComboBox.AdjustToContents) layH = QHBoxLayout() if dict_ui['label'] != "": lblW = QLabel(to_html(dict_ui['label'], frmt='bi'), self) layH.addWidget(lblW) layH.addStretch() layH.addWidget(lblOvfl) layH.addWidget(self.cmbOvfl) # layH.addStretch(1) layH.addWidget(lblQuant) layH.addWidget(self.cmbQuant) layH.setContentsMargins(0, 0, 0, 0) frmMain = QFrame(self) frmMain.setLayout(layH) layVMain = QVBoxLayout() # Widget main layout layVMain.addWidget(frmMain) layVMain.setContentsMargins(0, 0, 0, 0) # *params['wdg_margins']) self.setLayout(layVMain) # ---------------------------------------------------------------------- # INITIAL SETTINGS # ---------------------------------------------------------------------- self.ovfl = qget_cmb_box(self.cmbOvfl, data=False) self.quant = qget_cmb_box(self.cmbQuant, data=False) frmMain.setEnabled(dict_ui['enabled']) frmMain.setVisible(dict_ui['visible']) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.cmbOvfl.currentIndexChanged.connect(self.ui2dict) self.cmbQuant.currentIndexChanged.connect(self.ui2dict) # -------------------------------------------------------------------------- def ui2dict(self): """ Update the quantization dict and the attributes `self.ovfl` and `self.quant` from the UI """ self.ovfl = self.cmbOvfl.currentText() self.quant = self.cmbQuant.currentText() self.q_dict.update({'ovfl': self.ovfl, 'quant': self.quant}) if self.sender(): obj_name = self.sender().objectName() dict_sig = {'wdg_name': self.wdg_name, 'ui': obj_name} self.emit(dict_sig) # -------------------------------------------------------------------------- def dict2ui(self, q_dict): """ Update UI from passed dictionary """ pass
class InputTabWidgets(QWidget): """ Create a tabbed widget for all input subwidgets in the list ``fb.input_widgets_list``. This list is compiled at startup in :class:`pyfda.tree_builder.Tree_Builder`. """ # signals as class variables (shared between instances if more than one exists) # incoming, connected here to individual senders, passed on to process sigmals sig_rx = pyqtSignal(object) # outgoing, connected in receiver (pyfdax -> plot_tab_widgets) sig_tx = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(InputTabWidgets, self).__init__(parent) self._construct_UI() def _construct_UI(self): """ Initialize UI with tabbed subwidgets: Instantiate dynamically each widget from the dict `fb.input_classes` and try to - set the TabToolTip from the instance attribute `tool_tip` - set the tab label from the instance attribute `tab_label` for each widget. - connect the available signals of all subwidgets (not all widgets have both `sig_rx` and `sig_tx` signals). - `self.sig_rx` is distributed to all `inst.sig_rx` signals - all `inst.sig_tx` signals are collected in `self.sig_tx` - `self.sig_tx.connect(self.sig_rx)` distributes incoming signals (via pyfdax or coming from the input widgets) among all input widgets. In order to prevent infinite loops, every widget needs to block in- coming signals with its own name! """ tabWidget = QTabWidget(self) n_wdg = 0 # number and ... inst_wdg_str = "" # ... full names of successfully instantiated widgets for input_class in fb.input_classes: try: # fully qualified module name: mod_fq_name = fb.input_classes[input_class]['mod'] mod = importlib.import_module(mod_fq_name) wdg_class = getattr(mod, input_class) # and instantiate it inst = wdg_class(self) except ImportError as e: logger.warning( 'Class "{0}" could not be imported from {1}:\n{2}.'.format( input_class, mod_fq_name, e)) continue # unsuccessful, try next widget if hasattr(inst, "state") and inst.state == "deactivated": continue # with next widget if hasattr(inst, 'tab_label'): tabWidget.addTab(inst, inst.tab_label) else: tabWidget.addTab(inst, "not set") if hasattr(inst, 'tool_tip'): tabWidget.setTabToolTip(n_wdg, inst.tool_tip) if hasattr(inst, 'sig_tx'): inst.sig_tx.connect(self.sig_tx) if hasattr(inst, 'sig_rx'): self.sig_rx.connect(inst.sig_rx) n_wdg += 1 # successfully instantiated one more widget inst_wdg_str += '\t' + mod_fq_name + "." + input_class + '\n' if len(inst_wdg_str) == 0: logger.critical("No input widgets found!") sys.exit() else: logger.debug("Imported {0:d} input classes:\n{1}".format( n_wdg, inst_wdg_str)) # # TODO: document signal options # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- # self.sig_rx.connect(inst.sig_rx) # happens in _construct_UI() # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_tx.connect(self.sig_rx) # loop back to local inputs # self.sig_rx.connect(self.log_rx) # enable for debugging # When user has selected a different tab, trigger a redraw of current tab tabWidget.currentChanged.connect(self.current_tab_changed) # The following does not work: maybe current scope must be left? # tabWidget.currentChanged.connect(tabWidget.currentWidget().redraw) layVMain = QVBoxLayout() # setContentsMargins -> number of pixels between frame window border layVMain.setContentsMargins(*params['wdg_margins']) # -------------------------------------- if SCROLL: scroll = QScrollArea(self) scroll.setWidget(tabWidget) scroll.setWidgetResizable( True) # Size of monitored widget is allowed to grow layVMain.addWidget(scroll) else: layVMain.addWidget(tabWidget) # add the tabWidget directly self.setLayout(layVMain) # set the main layout of the window # ------------------------------------------------------------------------------ def log_rx(self, dict_sig=None): """ Enable `self.sig_rx.connect(self.log_rx)` above for debugging. """ if type(dict_sig) == dict: logger.warning("SIG_RX\n{0}".format(pprint_log(dict_sig))) else: logger.warning("empty dict") # ------------------------------------------------------------------------------ def current_tab_changed(self): self.emit({'ui_changed': 'tab'})
class FreqUnits(QWidget): """ Build and update widget for entering frequency unit, frequency range and sampling frequency f_S The following key-value pairs of the `fb.fil[0]` dict are modified: - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency values for labelling the frequency axis - `'f_S'` : The sampling frequency for referring frequency values to as a float - `'f_max'` : maximum frequency for scaling frequency axis - `'plt_fUnit'`: frequency unit as string - `'plt_tUnit'`: time unit as string - `'plt_fLabel'`: label for frequency axis - `'plt_tLabel'`: label for time axis """ # class variables (shared between instances if more than one exists) sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None, title="Frequency Units"): super(FreqUnits, self).__init__(parent) self.title = title self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() def _construct_UI(self): """ Construct the User Interface """ self.layVMain = QVBoxLayout() # Widget main layout f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz'] self.t_units = ['', 'T_S', 'T_S', 's', 'ms', r'$\mu$s', 'ns'] bfont = QFont() bfont.setBold(True) self.lblUnits = QLabel(self) self.lblUnits.setText("Freq. Unit") self.lblUnits.setFont(bfont) self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency self.lblF_S = QLabel(self) self.lblF_S.setText(to_html("f_S =", frmt='bi')) self.ledF_S = QLineEdit() self.ledF_S.setText(str(fb.fil[0]["f_S"])) self.ledF_S.setObjectName("f_S") self.ledF_S.installEventFilter(self) # filter events self.butLock = QToolButton(self) self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) self.butLock.setCheckable(True) self.butLock.setChecked(False) self.butLock.setToolTip( "<span><b>Unlocked:</b> When f_S is changed, all frequency related " "widgets are updated, normalized frequencies stay the same.<br />" "<b>Locked:</b> When f_S is changed, displayed absolute frequency " "values don't change but normalized frequencies do.</span>") # self.butLock.setStyleSheet("QToolButton:checked {font-weight:bold}") layHF_S = QHBoxLayout() layHF_S.addWidget(self.ledF_S) layHF_S.addWidget(self.butLock) self.cmbUnits = QComboBox(self) self.cmbUnits.setObjectName("cmbUnits") self.cmbUnits.addItems(f_units) self.cmbUnits.setToolTip( 'Select whether frequencies are specified w.r.t. \n' 'the sampling frequency "f_S", to the Nyquist frequency \n' 'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S ' 'but plots graphs over the frequency index k.') self.cmbUnits.setCurrentIndex(1) # self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))# # self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole) fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")] self.cmbFRange = QComboBox(self) self.cmbFRange.setObjectName("cmbFRange") for f in fRanges: self.cmbFRange.addItem(f[0], f[1]) self.cmbFRange.setToolTip("Select frequency range (whole or half).") self.cmbFRange.setCurrentIndex(0) # Combobox resizes with longest entry self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butSort = QToolButton(self) self.butSort.setText("Sort") self.butSort.setIcon(QIcon(':/sort-ascending.svg')) #self.butDelCells.setIconSize(q_icon_size) self.butSort.setCheckable(True) self.butSort.setChecked(True) self.butSort.setToolTip( "Sort frequencies in ascending order when pushed.") self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}") self.layHUnits = QHBoxLayout() self.layHUnits.addWidget(self.cmbUnits) self.layHUnits.addWidget(self.cmbFRange) self.layHUnits.addWidget(self.butSort) # Create a gridLayout consisting of QLabel and QLineEdit fields # for setting f_S, the units and the actual frequency specs: self.layGSpecWdg = QGridLayout() # sublayout for spec fields self.layGSpecWdg.addWidget(self.lblF_S, 1, 0) # self.layGSpecWdg.addWidget(self.ledF_S,1,1) self.layGSpecWdg.addLayout(layHF_S, 1, 1) self.layGSpecWdg.addWidget(self.lblUnits, 0, 0) self.layGSpecWdg.addLayout(self.layHUnits, 0, 1) frmMain = QFrame(self) frmMain.setLayout(self.layGSpecWdg) self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnits.currentIndexChanged.connect(self.update_UI) self.butLock.clicked.connect(self._lock_freqs) self.cmbFRange.currentIndexChanged.connect(self._freq_range) self.butSort.clicked.connect(self._store_sort_flag) # ---------------------------------------------------------------------- self.update_UI() # first-time initialization # ------------------------------------------------------------- def _lock_freqs(self): """ Lock / unlock frequency entries: The values of frequency related widgets are stored in normalized form (w.r.t. sampling frequency)`fb.fil[0]['f_S']`. When the sampling frequency changes, absolute frequencies displayed in the widgets change their values. Most of the time, this is the desired behaviour, the properties of discrete time systems or signals are usually defined by the normalized frequencies. When the effect of varying the sampling frequency is to be analyzed, the displayed values in the widgets can be locked by pressing the Lock button. After changing the sampling frequency, normalized frequencies have to be rescaled like `f_a *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']` to maintain the displayed value `f_a * f_S`. This has to be accomplished by each frequency widget (currently, these are freq_specs and freq_units). The setting is stored as bool in the global dict entry `fb.fil[0]['freq_locked'`, the signal 'view_changed':'f_S' is emitted. """ if self.butLock.isChecked(): # Lock has been activated, keep displayed frequencies locked fb.fil[0]['freq_locked'] = True self.butLock.setIcon(QIcon(':/lock-locked.svg')) else: # Lock has been unlocked, scale displayed frequencies with f_S fb.fil[0]['freq_locked'] = False self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) self.emit({'view_changed': 'f_S'}) # ------------------------------------------------------------- def update_UI(self): """ update_UI is called - during init - when the unit combobox is changed Set various scale factors and labels depending on the setting of the unit combobox. Update the freqSpecsRange and finally, emit 'view_changed':'f_S' signal """ f_unit = str(self.cmbUnits.currentText()) # selected frequency unit idx = self.cmbUnits.currentIndex() # and its index is_normalized_freq = f_unit in {"f_S", "f_Ny", "k"} self.ledF_S.setVisible(not is_normalized_freq) # only vis. when self.lblF_S.setVisible(not is_normalized_freq) # not normalized self.butLock.setVisible(not is_normalized_freq) f_S_scale = 1 # default setting for f_S scale if is_normalized_freq: # store current sampling frequency to restore it when returning to # unnormalized frequencies self.fs_old = fb.fil[0]['f_S'] if f_unit == "f_S": # normalized to f_S fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1. fb.fil[0]['T_S'] = 1. f_label = r"$F = f\, /\, f_S = \Omega \, /\, 2 \mathrm{\pi} \; \rightarrow$" t_label = r"$n = t\, /\, T_S \; \rightarrow$" elif f_unit == "f_Ny": # normalized to f_nyq = f_S / 2 fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2. fb.fil[0]['T_S'] = 1. f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$" t_label = r"$n = t\, /\, T_S \; \rightarrow$" else: # frequency index k, fb.fil[0]['f_S'] = 1. fb.fil[0]['T_S'] = 1. fb.fil[0]['f_max'] = params['N_FFT'] f_label = r"$k \; \rightarrow$" t_label = r"$n\; \rightarrow$" self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) else: # Hz, kHz, ... # Restore sampling frequency when returning from f_S / f_Ny / k if fb.fil[0]['freq_specs_unit'] in { "f_S", "f_Ny", "k" }: # previous setting normalized? fb.fil[0]['f_S'] = fb.fil[0][ 'f_max'] = self.fs_old # yes, restore prev. fb.fil[0][ 'T_S'] = 1. / self.fs_old # settings for sampling frequency self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) if f_unit == "Hz": f_S_scale = 1. elif f_unit == "kHz": f_S_scale = 1.e3 elif f_unit == "MHz": f_S_scale = 1.e6 elif f_unit == "GHz": f_S_scale = 1.e9 else: logger.warning("Unknown frequency unit {0}".format(f_unit)) f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$" t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$" if f_unit == "k": plt_f_unit = "f_S" else: plt_f_unit = f_unit fb.fil[0].update({'f_S_scale': f_S_scale}) # scale factor for f_S (Hz, kHz, ...) fb.fil[0].update({'freq_specs_unit': f_unit}) # frequency unit # time and frequency unit as string e.g. for plot axis labeling fb.fil[0].update({"plt_fUnit": plt_f_unit}) fb.fil[0].update({"plt_tUnit": self.t_units[idx]}) # complete plot axis labels including unit and arrow fb.fil[0].update({"plt_fLabel": f_label}) fb.fil[0].update({"plt_tLabel": t_label}) self._freq_range( emit=False) # update f_lim setting without emitting signal self.emit({'view_changed': 'f_S'}) # ------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit `f_S` widget. 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 with full precision (only if `spec_edited`== True) and display the stored value in selected format. Emit 'view_changed':'f_S' """ def _store_entry(): """ Update filter dictionary, set line edit entry with reduced precision again. """ if self.spec_edited: fb.fil[0].update({'f_S_prev': fb.fil[0]['f_S']}) fb.fil[0].update({ 'f_S': safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos') }) fb.fil[0].update({'T_S': 1. / fb.fil[0]['f_S']}) fb.fil[0].update({'f_max': fb.fil[0]['f_S']}) self._freq_range(emit=False) # update plotting range self.emit({'view_changed': 'f_S'}) self.spec_edited = False # reset flag, changed entry has been saved if source.objectName() == 'f_S': if event.type() == QEvent.FocusIn: self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: _store_entry() elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.FocusOut: _store_entry() source.setText(params['FMT'].format( fb.fil[0]['f_S'])) # reduced prec. # Call base class method to continue normal event processing: return super(FreqUnits, self).eventFilter(source, event) # ------------------------------------------------------------- def _freq_range(self, emit=True): """ Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S or for double-sided spectrum between -f_S/2 and f_S/2 Emit 'view_changed':'f_range' when `emit=True` """ if type(emit) == int: # signal was emitted by combobox emit = True rangeType = qget_cmb_box(self.cmbFRange) fb.fil[0].update({'freqSpecsRangeType': rangeType}) f_max = fb.fil[0]["f_max"] if rangeType == 'whole': f_lim = [0, f_max] elif rangeType == 'sym': f_lim = [-f_max / 2., f_max / 2.] else: f_lim = [0, f_max / 2.] fb.fil[0]['freqSpecsRange'] = f_lim # store settings in dict if emit: self.emit({'view_changed': 'f_range'}) # ------------------------------------------------------------- def load_dict(self): """ Reload comboBox settings and textfields from filter dictionary Block signals during update of combobox / lineedit widgets """ self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) self.cmbUnits.blockSignals(True) idx = self.cmbUnits.findText( fb.fil[0]['freq_specs_unit']) # get and set self.cmbUnits.setCurrentIndex(idx) # index for freq. unit combo box self.cmbUnits.blockSignals(False) self.cmbFRange.blockSignals(True) idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType']) self.cmbFRange.setCurrentIndex(idx) # set frequency range self.cmbFRange.blockSignals(False) self.butSort.blockSignals(True) self.butSort.setChecked(fb.fil[0]['freq_specs_sort']) self.butSort.blockSignals(False) # ------------------------------------------------------------- def _store_sort_flag(self): """ Store sort flag in filter dict and emit 'specs_changed':'f_sort' when sort button is checked. """ fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked() if self.butSort.isChecked(): self.emit({'specs_changed': 'f_sort'})
class PlotImpz_UI(QWidget): """ Create the UI for the PlotImpz class """ # incoming: not implemented at the moment, update_N is triggered directly # by plot_impz # sig_rx = pyqtSignal(object) # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx') sig_tx = pyqtSignal(object) # outgoing to local fft window sig_tx_fft = pyqtSignal(object) def __init__(self, parent): """ Pass instance `parent` of parent class (FilterCoeffs) """ super(PlotImpz_UI, self).__init__(parent) """ Intitialize the widget, consisting of: - top chkbox row - coefficient table - two bottom rows with action buttons """ # initial settings self.N_start = 0 self.N_user = 0 self.N = 0 # time self.plt_time_resp = "Stem" self.plt_time_stim = "None" self.plt_time_stmq = "None" self.plt_time_spgr = "None" self.bottom_t = -80 # initial value for log. scale (time) self.nfft_spgr_time = 256 # number of fft points per spectrogram segment self.ovlp_spgr_time = 128 # number of overlap points between spectrogram segments self.mode_spgr_time = "magnitude" # stimuli self.stim = "Impulse" self.chirp_method = 'Linear' self.noise = "None" self.f1 = 0.02 self.f2 = 0.03 self.A1 = 1.0 self.A2 = 0.0 self.phi1 = self.phi2 = 0 self.noi = 0.1 self.noise = 'none' self.DC = 0.0 self.stim_formula = "A1 * abs(sin(2 * pi * f1 * n))" # frequency self.plt_freq_resp = "Line" self.plt_freq_stim = "None" self.plt_freq_stmq = "None" self.bottom_f = -120 # initial value for log. scale self.param = None # dictionary for fft window settings self.win_dict = fb.fil[0]['win_fft'] self.fft_window = None # handle for FFT window pop-up widget self.window_name = "Rectangular" self._construct_UI() self._enable_stim_widgets() self.update_N(emit=False) # also updates window function self._update_noi() def _construct_UI(self): # ----------- --------------------------------------------------- # Run control widgets # --------------------------------------------------------------- self.chk_auto_run = QCheckBox("Auto", self) self.chk_auto_run.setObjectName("chk_auto_run") self.chk_auto_run.setToolTip("<span>Update response automatically when " "parameters have been changed.</span>") self.chk_auto_run.setChecked(True) self.but_run = QPushButton(self) self.but_run.setText("RUN") self.but_run.setToolTip("Run simulation") self.but_run.setEnabled(not self.chk_auto_run.isChecked()) self.cmb_sim_select = QComboBox(self) self.cmb_sim_select.addItems(["Float","Fixpoint"]) qset_cmb_box(self.cmb_sim_select, "Float") self.cmb_sim_select.setToolTip("<span>Simulate floating-point or fixpoint response." "</span>") self.lbl_N_points = QLabel(to_html("N", frmt='bi') + " =", self) self.led_N_points = QLineEdit(self) self.led_N_points.setText(str(self.N)) self.led_N_points.setToolTip("<span>Number of displayed data points. " "<i>N</i> = 0 tries to choose for you.</span>") self.lbl_N_start = QLabel(to_html("N_0", frmt='bi') + " =", self) self.led_N_start = QLineEdit(self) self.led_N_start.setText(str(self.N_start)) self.led_N_start.setToolTip("<span>First point to plot.</span>") self.chk_fx_scale = QCheckBox("Int. scale", self) self.chk_fx_scale.setObjectName("chk_fx_scale") self.chk_fx_scale.setToolTip("<span>Display data with integer (fixpoint) scale.</span>") self.chk_fx_scale.setChecked(False) self.chk_stim_options = QCheckBox("Stim. Options", self) self.chk_stim_options.setObjectName("chk_stim_options") self.chk_stim_options.setToolTip("<span>Show stimulus options.</span>") self.chk_stim_options.setChecked(True) self.but_fft_win = QPushButton(self) self.but_fft_win.setText("WIN FFT") self.but_fft_win.setToolTip('<span> time and frequency response of FFT Window ' '(can be modified in the "Frequency" tab)</span>') self.but_fft_win.setCheckable(True) self.but_fft_win.setChecked(False) layH_ctrl_run = QHBoxLayout() layH_ctrl_run.addWidget(self.but_run) #layH_ctrl_run.addWidget(self.lbl_sim_select) layH_ctrl_run.addWidget(self.cmb_sim_select) layH_ctrl_run.addWidget(self.chk_auto_run) layH_ctrl_run.addStretch(1) layH_ctrl_run.addWidget(self.lbl_N_start) layH_ctrl_run.addWidget(self.led_N_start) layH_ctrl_run.addStretch(1) layH_ctrl_run.addWidget(self.lbl_N_points) layH_ctrl_run.addWidget(self.led_N_points) layH_ctrl_run.addStretch(2) layH_ctrl_run.addWidget(self.chk_fx_scale) layH_ctrl_run.addStretch(2) layH_ctrl_run.addWidget(self.chk_stim_options) layH_ctrl_run.addStretch(2) layH_ctrl_run.addWidget(self.but_fft_win) layH_ctrl_run.addStretch(10) #layH_ctrl_run.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_run = QWidget(self) self.wdg_ctrl_run.setLayout(layH_ctrl_run) # --- end of run control ---------------------------------------- # ----------- --------------------------------------------------- # Controls for time domain # --------------------------------------------------------------- plot_styles_list = ["None","Dots","Line","Line*","Stem","Stem*","Step","Step*"] lbl_plt_time_title = QLabel("<b>View:</b>", self) self.lbl_plt_time_stim = QLabel(to_html("Stimulus x", frmt='bi'), self) self.cmb_plt_time_stim = QComboBox(self) self.cmb_plt_time_stim.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_time_stim, self.plt_time_stim) self.cmb_plt_time_stim.setToolTip("<span>Plot style for stimulus.</span>") self.lbl_plt_time_stmq = QLabel(to_html(" Fixp. Stim. x_Q", frmt='bi'), self) self.cmb_plt_time_stmq = QComboBox(self) self.cmb_plt_time_stmq.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_time_stmq, self.plt_time_stmq) self.cmb_plt_time_stmq.setToolTip("<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>") lbl_plt_time_resp = QLabel(to_html(" Response y", frmt='bi'), self) self.cmb_plt_time_resp = QComboBox(self) self.cmb_plt_time_resp.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_time_resp, self.plt_time_resp) self.cmb_plt_time_resp.setToolTip("<span>Plot style for response.</span>") lbl_win_time = QLabel(to_html(" FFT Window", frmt='bi'), self) self.chk_win_time = QCheckBox(self) self.chk_win_time.setObjectName("chk_win_time") self.chk_win_time.setToolTip('<span>Show FFT windowing function (can be modified in the "Frequency" tab).</span>') self.chk_win_time.setChecked(False) lbl_log_time = QLabel(to_html("dB", frmt='b'), self) self.chk_log_time = QCheckBox(self) self.chk_log_time.setObjectName("chk_log_time") self.chk_log_time.setToolTip("<span>Logarithmic scale for y-axis.</span>") self.chk_log_time.setChecked(False) self.lbl_log_bottom_time = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_time.setVisible(True) self.led_log_bottom_time = QLineEdit(self) self.led_log_bottom_time.setText(str(self.bottom_t)) self.led_log_bottom_time.setToolTip("<span>Minimum display value for time " "and spectrogram plots with log. scale.</span>") self.led_log_bottom_time.setVisible(True) lbl_plt_time_spgr = QLabel(to_html(" Spectrogram", frmt='bi'), self) self.cmb_plt_time_spgr = QComboBox(self) self.cmb_plt_time_spgr.addItems(["None", "x[n]", "x_q[n]", "y[n]"]) qset_cmb_box(self.cmb_plt_time_spgr, self.plt_time_spgr) self.cmb_plt_time_spgr.setToolTip("<span>Show Spectrogram for selected signal.</span>") spgr_en = self.plt_time_spgr != "None" self.lbl_log_spgr_time = QLabel(to_html(" dB", frmt='b'), self) self.lbl_log_spgr_time.setVisible(spgr_en) self.chk_log_spgr_time = QCheckBox(self) self.chk_log_spgr_time.setObjectName("chk_log_spgr") self.chk_log_spgr_time.setToolTip("<span>Logarithmic scale for spectrogram.</span>") self.chk_log_spgr_time.setChecked(True) self.chk_log_spgr_time.setVisible(spgr_en) self.lbl_nfft_spgr_time = QLabel(to_html(" N_FFT =", frmt='bi'), self) self.lbl_nfft_spgr_time.setVisible(spgr_en) self.led_nfft_spgr_time = QLineEdit(self) self.led_nfft_spgr_time.setText(str(self.nfft_spgr_time)) self.led_nfft_spgr_time.setToolTip("<span>Number of FFT points per spectrogram segment.</span>") self.led_nfft_spgr_time.setVisible(spgr_en) self.lbl_ovlp_spgr_time = QLabel(to_html(" N_OVLP =", frmt='bi'), self) self.lbl_ovlp_spgr_time.setVisible(spgr_en) self.led_ovlp_spgr_time = QLineEdit(self) self.led_ovlp_spgr_time.setText(str(self.ovlp_spgr_time)) self.led_ovlp_spgr_time.setToolTip("<span>Number of overlap data points between spectrogram segments.</span>") self.led_ovlp_spgr_time.setVisible(spgr_en) self.lbl_mode_spgr_time = QLabel(to_html(" Mode", frmt='bi'), self) self.lbl_mode_spgr_time.setVisible(spgr_en) self.cmb_mode_spgr_time = QComboBox(self) spgr_modes = [("PSD","psd"), ("Mag.","magnitude"),\ ("Angle","angle"), ("Phase","phase")] for i in spgr_modes: self.cmb_mode_spgr_time.addItem(*i) qset_cmb_box(self.cmb_mode_spgr_time, self.mode_spgr_time, data=True) self.cmb_mode_spgr_time.setToolTip("<span>Spectrogram display mode.</span>") self.cmb_mode_spgr_time.setVisible(spgr_en) self.lbl_byfs_spgr_time = QLabel(to_html(" per f_S", frmt='b'), self) self.lbl_byfs_spgr_time.setVisible(spgr_en) self.chk_byfs_spgr_time = QCheckBox(self) self.chk_byfs_spgr_time.setObjectName("chk_log_spgr") self.chk_byfs_spgr_time.setToolTip("<span>Display spectral density i.e. scale by f_S</span>") self.chk_byfs_spgr_time.setChecked(True) self.chk_byfs_spgr_time.setVisible(spgr_en) # self.lbl_colorbar_time = QLabel(to_html(" Col.bar", frmt='b'), self) # self.lbl_colorbar_time.setVisible(spgr_en) # self.chk_colorbar_time = QCheckBox(self) # self.chk_colorbar_time.setObjectName("chk_colorbar_time") # self.chk_colorbar_time.setToolTip("<span>Enable colorbar</span>") # self.chk_colorbar_time.setChecked(True) # self.chk_colorbar_time.setVisible(spgr_en) self.chk_fx_limits = QCheckBox("Min/max.", self) self.chk_fx_limits.setObjectName("chk_fx_limits") self.chk_fx_limits.setToolTip("<span>Display limits of fixpoint range.</span>") self.chk_fx_limits.setChecked(False) layH_ctrl_time = QHBoxLayout() layH_ctrl_time.addWidget(lbl_plt_time_title) layH_ctrl_time.addStretch(1) layH_ctrl_time.addWidget(self.lbl_plt_time_stim) layH_ctrl_time.addWidget(self.cmb_plt_time_stim) # layH_ctrl_time.addWidget(self.lbl_plt_time_stmq) layH_ctrl_time.addWidget(self.cmb_plt_time_stmq) # layH_ctrl_time.addWidget(lbl_plt_time_resp) layH_ctrl_time.addWidget(self.cmb_plt_time_resp) # layH_ctrl_time.addWidget(lbl_win_time) layH_ctrl_time.addWidget(self.chk_win_time) layH_ctrl_time.addStretch(1) layH_ctrl_time.addWidget(lbl_log_time) layH_ctrl_time.addWidget(self.chk_log_time) layH_ctrl_time.addWidget(self.lbl_log_bottom_time) layH_ctrl_time.addWidget(self.led_log_bottom_time) # layH_ctrl_time.addStretch(1) # layH_ctrl_time.addWidget(lbl_plt_time_spgr) layH_ctrl_time.addWidget(self.cmb_plt_time_spgr) layH_ctrl_time.addWidget(self.lbl_log_spgr_time) layH_ctrl_time.addWidget(self.chk_log_spgr_time) layH_ctrl_time.addWidget(self.lbl_nfft_spgr_time) layH_ctrl_time.addWidget(self.led_nfft_spgr_time) layH_ctrl_time.addWidget(self.lbl_ovlp_spgr_time) layH_ctrl_time.addWidget(self.led_ovlp_spgr_time) layH_ctrl_time.addWidget(self.lbl_mode_spgr_time) layH_ctrl_time.addWidget(self.cmb_mode_spgr_time) layH_ctrl_time.addWidget(self.lbl_byfs_spgr_time) layH_ctrl_time.addWidget(self.chk_byfs_spgr_time) layH_ctrl_time.addStretch(2) layH_ctrl_time.addWidget(self.chk_fx_limits) layH_ctrl_time.addStretch(10) #layH_ctrl_time.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_time = QWidget(self) self.wdg_ctrl_time.setLayout(layH_ctrl_time) # ---- end time domain ------------------ # --------------------------------------------------------------- # Controls for frequency domain # --------------------------------------------------------------- lbl_plt_freq_title = QLabel("<b>View:</b>", self) self.lbl_plt_freq_stim = QLabel(to_html("Stimulus X", frmt='bi'), self) self.cmb_plt_freq_stim = QComboBox(self) self.cmb_plt_freq_stim.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_freq_stim, self.plt_freq_stim) self.cmb_plt_freq_stim.setToolTip("<span>Plot style for stimulus.</span>") self.lbl_plt_freq_stmq = QLabel(to_html(" Fixp. Stim. X_Q", frmt='bi'), self) self.cmb_plt_freq_stmq = QComboBox(self) self.cmb_plt_freq_stmq.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_freq_stmq, self.plt_freq_stmq) self.cmb_plt_freq_stmq.setToolTip("<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>") lbl_plt_freq_resp = QLabel(to_html(" Response Y", frmt='bi'), self) self.cmb_plt_freq_resp = QComboBox(self) self.cmb_plt_freq_resp.addItems(plot_styles_list) qset_cmb_box(self.cmb_plt_freq_resp, self.plt_freq_resp) self.cmb_plt_freq_resp.setToolTip("<span>Plot style for response.</span>") lbl_log_freq = QLabel(to_html("dB", frmt='b'), self) self.chk_log_freq = QCheckBox(self) self.chk_log_freq.setObjectName("chk_log_freq") self.chk_log_freq.setToolTip("<span>Logarithmic scale for y-axis.</span>") self.chk_log_freq.setChecked(True) self.lbl_log_bottom_freq = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_freq.setVisible(self.chk_log_freq.isChecked()) self.led_log_bottom_freq = QLineEdit(self) self.led_log_bottom_freq.setText(str(self.bottom_f)) self.led_log_bottom_freq.setToolTip("<span>Minimum display value for log. scale.</span>") self.led_log_bottom_freq.setVisible(self.chk_log_freq.isChecked()) if not self.chk_log_freq.isChecked(): self.bottom_f = 0 lbl_re_im_freq = QLabel(to_html("Re / Im", frmt='b'), self) self.chk_re_im_freq = QCheckBox(self) self.chk_re_im_freq.setObjectName("chk_re_im_freq") self.chk_re_im_freq.setToolTip("<span>Show real and imaginary part of spectrum</span>") self.chk_re_im_freq.setChecked(False) self.lbl_win_fft = QLabel(to_html("Window", frmt='bi'), self) self.cmb_win_fft = QComboBox(self) self.cmb_win_fft.addItems(get_window_names()) self.cmb_win_fft.setToolTip("FFT window type.") qset_cmb_box(self.cmb_win_fft, self.window_name) self.cmb_win_fft_variant = QComboBox(self) self.cmb_win_fft_variant.setToolTip("FFT window variant.") self.cmb_win_fft_variant.setVisible(False) self.lblWinPar1 = QLabel("Param1") self.ledWinPar1 = QLineEdit(self) self.ledWinPar1.setText("1") self.ledWinPar1.setObjectName("ledWinPar1") self.lblWinPar2 = QLabel("Param2") self.ledWinPar2 = QLineEdit(self) self.ledWinPar2.setText("2") self.ledWinPar2.setObjectName("ledWinPar2") self.chk_Hf = QCheckBox(self) self.chk_Hf.setObjectName("chk_Hf") self.chk_Hf.setToolTip("<span>Show ideal frequency response, calculated " "from the filter coefficients.</span>") self.chk_Hf.setChecked(False) self.chk_Hf_lbl = QLabel(to_html("H_id (f)", frmt="bi"), self) lbl_show_info_freq = QLabel(to_html("Info", frmt='b'), self) self.chk_show_info_freq = QCheckBox(self) self.chk_show_info_freq.setObjectName("chk_show_info_freq") self.chk_show_info_freq.setToolTip("<span>Show infos about signal power " "and window properties.</span>") self.chk_show_info_freq.setChecked(False) layH_ctrl_freq = QHBoxLayout() layH_ctrl_freq.addWidget(lbl_plt_freq_title) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(self.lbl_plt_freq_stim) layH_ctrl_freq.addWidget(self.cmb_plt_freq_stim) # layH_ctrl_freq.addWidget(self.lbl_plt_freq_stmq) layH_ctrl_freq.addWidget(self.cmb_plt_freq_stmq) # layH_ctrl_freq.addWidget(lbl_plt_freq_resp) layH_ctrl_freq.addWidget(self.cmb_plt_freq_resp) # layH_ctrl_freq.addWidget(self.chk_Hf_lbl) layH_ctrl_freq.addWidget(self.chk_Hf) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(lbl_log_freq) layH_ctrl_freq.addWidget(self.chk_log_freq) layH_ctrl_freq.addWidget(self.lbl_log_bottom_freq) layH_ctrl_freq.addWidget(self.led_log_bottom_freq) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(lbl_re_im_freq) layH_ctrl_freq.addWidget(self.chk_re_im_freq) layH_ctrl_freq.addStretch(2) layH_ctrl_freq.addWidget(self.lbl_win_fft) layH_ctrl_freq.addWidget(self.cmb_win_fft) layH_ctrl_freq.addWidget(self.cmb_win_fft_variant) layH_ctrl_freq.addWidget(self.lblWinPar1) layH_ctrl_freq.addWidget(self.ledWinPar1) layH_ctrl_freq.addWidget(self.lblWinPar2) layH_ctrl_freq.addWidget(self.ledWinPar2) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(lbl_show_info_freq) layH_ctrl_freq.addWidget(self.chk_show_info_freq) layH_ctrl_freq.addStretch(10) #layH_ctrl_freq.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_freq = QWidget(self) self.wdg_ctrl_freq.setLayout(layH_ctrl_freq) # ---- end Frequency Domain ------------------ # --------------------------------------------------------------- # Controls for stimuli # --------------------------------------------------------------- lbl_title_stim = QLabel("<b>Stimulus:</b>", self) self.lblStimulus = QLabel(to_html("Type", frmt='bi'), self) self.cmbStimulus = QComboBox(self) self.cmbStimulus.addItems(["None","Impulse","Step","StepErr","Cos","Sine", "Chirp", "Triang","Saw","Rect","Comb","AM","PM / FM","Formula"]) self.cmbStimulus.setToolTip("Stimulus type.") qset_cmb_box(self.cmbStimulus, self.stim) self.chk_stim_bl = QCheckBox("BL", self) self.chk_stim_bl.setToolTip("<span>The signal is bandlimited to the Nyquist frequency " "to avoid aliasing. However, it is much slower to generate " "than the regular version.</span>") self.chk_stim_bl.setChecked(True) self.chk_stim_bl.setObjectName("stim_bl") self.cmbChirpMethod = QComboBox(self) for t in [("Lin","Linear"),("Square","Quadratic"),("Log", "Logarithmic"), ("Hyper", "Hyperbolic")]: self.cmbChirpMethod.addItem(*t) qset_cmb_box(self.cmbChirpMethod, self.chirp_method, data=False) self.chk_scale_impz_f = QCheckBox("Scale", self) self.chk_scale_impz_f.setToolTip("<span>Scale the FFT of the impulse response with <i>N<sub>FFT</sub></i> " "so that it has the same magnitude as |H(f)|. DC and Noise need to be " "turned off.</span>") self.chk_scale_impz_f.setChecked(True) self.chk_scale_impz_f.setObjectName("scale_impz_f") self.lblDC = QLabel(to_html("DC =", frmt='bi'), self) self.ledDC = QLineEdit(self) self.ledDC.setText(str(self.DC)) self.ledDC.setToolTip("DC Level") self.ledDC.setObjectName("stimDC") layHCmbStim = QHBoxLayout() layHCmbStim.addWidget(self.cmbStimulus) layHCmbStim.addWidget(self.chk_stim_bl) layHCmbStim.addWidget(self.chk_scale_impz_f) layHCmbStim.addWidget(self.cmbChirpMethod) #---------------------------------------------- self.lblAmp1 = QLabel(to_html(" A_1", frmt='bi') + " =", self) self.ledAmp1 = QLineEdit(self) self.ledAmp1.setText(str(self.A1)) self.ledAmp1.setToolTip("Stimulus amplitude, complex values like 3j - 1 are allowed") self.ledAmp1.setObjectName("stimAmp1") self.lblAmp2 = QLabel(to_html(" A_2", frmt='bi') + " =", self) self.ledAmp2 = QLineEdit(self) self.ledAmp2.setText(str(self.A2)) self.ledAmp2.setToolTip("Stimulus amplitude 2, complex values like 3j - 1 are allowed") self.ledAmp2.setObjectName("stimAmp2") #---------------------------------------------- self.lblPhi1 = QLabel(to_html(" φ_1", frmt='bi') + " =", self) self.ledPhi1 = QLineEdit(self) self.ledPhi1.setText(str(self.phi1)) self.ledPhi1.setToolTip("Stimulus phase") self.ledPhi1.setObjectName("stimPhi1") self.lblPhU1 = QLabel(to_html("°", frmt='b'), self) self.lblPhi2 = QLabel(to_html(" φ_2", frmt='bi') + " =", self) self.ledPhi2 = QLineEdit(self) self.ledPhi2.setText(str(self.phi2)) self.ledPhi2.setToolTip("Stimulus phase 2") self.ledPhi2.setObjectName("stimPhi2") self.lblPhU2 = QLabel(to_html("°", frmt='b'), self) #---------------------------------------------- self.lblFreq1 = QLabel(to_html(" f_1", frmt='bi') + " =", self) self.ledFreq1 = QLineEdit(self) self.ledFreq1.setText(str(self.f1)) self.ledFreq1.setToolTip("Stimulus frequency 1") self.ledFreq1.setObjectName("stimFreq1") self.lblFreqUnit1 = QLabel("f_S", self) self.lblFreq2 = QLabel(to_html(" f_2", frmt='bi') + " =", self) self.ledFreq2 = QLineEdit(self) self.ledFreq2.setText(str(self.f2)) self.ledFreq2.setToolTip("Stimulus frequency 2") self.ledFreq2.setObjectName("stimFreq2") self.lblFreqUnit2 = QLabel("f_S", self) #---------------------------------------------- self.lblNoise = QLabel(to_html(" Noise", frmt='bi'), self) self.cmbNoise = QComboBox(self) self.cmbNoise.addItems(["None","Gauss","Uniform","PRBS"]) self.cmbNoise.setToolTip("Type of additive noise.") qset_cmb_box(self.cmbNoise, self.noise) self.lblNoi = QLabel("not initialized", self) self.ledNoi = QLineEdit(self) self.ledNoi.setText(str(self.noi)) self.ledNoi.setToolTip("not initialized") self.ledNoi.setObjectName("stimNoi") layGStim = QGridLayout() layGStim.addWidget(self.lblStimulus, 0, 0) layGStim.addWidget(self.lblDC, 1, 0) layGStim.addLayout(layHCmbStim, 0, 1) layGStim.addWidget(self.ledDC, 1, 1) layGStim.addWidget(self.lblAmp1, 0, 2) layGStim.addWidget(self.lblAmp2, 1, 2) layGStim.addWidget(self.ledAmp1, 0, 3) layGStim.addWidget(self.ledAmp2, 1, 3) layGStim.addWidget(self.lblPhi1, 0, 4) layGStim.addWidget(self.lblPhi2, 1, 4) layGStim.addWidget(self.ledPhi1, 0, 5) layGStim.addWidget(self.ledPhi2, 1, 5) layGStim.addWidget(self.lblPhU1, 0, 6) layGStim.addWidget(self.lblPhU2, 1, 6) layGStim.addWidget(self.lblFreq1, 0, 7) layGStim.addWidget(self.lblFreq2, 1, 7) layGStim.addWidget(self.ledFreq1, 0, 8) layGStim.addWidget(self.ledFreq2, 1, 8) layGStim.addWidget(self.lblFreqUnit1, 0, 9) layGStim.addWidget(self.lblFreqUnit2, 1, 9) layGStim.addWidget(self.lblNoise, 0, 10) layGStim.addWidget(self.lblNoi, 1, 10) layGStim.addWidget(self.cmbNoise, 0, 11) layGStim.addWidget(self.ledNoi, 1, 11) #---------------------------------------------- self.lblStimFormula = QLabel(to_html("x =", frmt='bi'), self) self.ledStimFormula = QLineEdit(self) self.ledStimFormula.setText(str(self.stim_formula)) self.ledStimFormula.setToolTip("<span>Enter formula for stimulus in numexpr syntax" "</span>") self.ledStimFormula.setObjectName("stimFormula") layH_ctrl_stim_formula = QHBoxLayout() layH_ctrl_stim_formula.addWidget(self.lblStimFormula) layH_ctrl_stim_formula.addWidget(self.ledStimFormula,10) #---------------------------------------------- #layG_ctrl_stim = QGridLayout() layH_ctrl_stim_par = QHBoxLayout() layH_ctrl_stim_par.addLayout(layGStim) layV_ctrl_stim = QVBoxLayout() layV_ctrl_stim.addLayout(layH_ctrl_stim_par) layV_ctrl_stim.addLayout(layH_ctrl_stim_formula) layH_ctrl_stim = QHBoxLayout() layH_ctrl_stim.addWidget(lbl_title_stim) layH_ctrl_stim.addStretch(1) layH_ctrl_stim.addLayout(layV_ctrl_stim) layH_ctrl_stim.addStretch(10) self.wdg_ctrl_stim = QWidget(self) self.wdg_ctrl_stim.setLayout(layH_ctrl_stim) # --------- end stimuli --------------------------------- # frequency widgets require special handling as they are scaled with f_s self.ledFreq1.installEventFilter(self) self.ledFreq2.installEventFilter(self) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- # --- run control --- self.led_N_start.editingFinished.connect(self.update_N) self.led_N_points.editingFinished.connect(self.update_N) # --- frequency control --- # careful! currentIndexChanged passes the current index to _update_win_fft self.cmb_win_fft.currentIndexChanged.connect(self._update_win_fft) self.ledWinPar1.editingFinished.connect(self._read_param1) self.ledWinPar2.editingFinished.connect(self._read_param2) # --- stimulus control --- self.chk_stim_options.clicked.connect(self._show_stim_options) self.chk_stim_bl.clicked.connect(self._enable_stim_widgets) self.cmbStimulus.currentIndexChanged.connect(self._enable_stim_widgets) self.cmbNoise.currentIndexChanged.connect(self._update_noi) self.ledNoi.editingFinished.connect(self._update_noi) self.ledAmp1.editingFinished.connect(self._update_amp1) self.ledAmp2.editingFinished.connect(self._update_amp2) self.ledPhi1.editingFinished.connect(self._update_phi1) self.ledPhi2.editingFinished.connect(self._update_phi2) self.cmbChirpMethod.currentIndexChanged.connect(self._update_chirp_method) self.ledDC.editingFinished.connect(self._update_DC) self.ledStimFormula.editingFinished.connect(self._update_stim_formula) #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the monitored widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (``QEvent.FocusIn``), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (``QEvent.FocusOut``), store current value normalized to f_S with full precision (only if ``spec_edited == True``) and display the stored value in selected format """ def _store_entry(source): if self.spec_edited: if source.objectName() == "stimFreq1": self.f1 = safe_eval(source.text(), self.f1 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": self.f2 = safe_eval(source.text(), self.f2 * fb.fil[0]['f_S'], return_type='float') / fb.fil[0]['f_S'] source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) self.spec_edited = False # reset flag self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim'}) # if isinstance(source, QLineEdit): # if source.objectName() in {"stimFreq1","stimFreq2"}: if event.type() in {QEvent.FocusIn,QEvent.KeyPress, QEvent.FocusOut}: if event.type() == QEvent.FocusIn: self.spec_edited = False self.load_fs() elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {Qt.Key_Return, Qt.Key_Enter}: _store_entry(source) elif key == Qt.Key_Escape: # revert changes self.spec_edited = False if source.objectName() == "stimFreq1": source.setText(str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) elif source.objectName() == "stimFreq2": source.setText(str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) elif event.type() == QEvent.FocusOut: _store_entry(source) # Call base class method to continue normal event processing: return super(PlotImpz_UI, self).eventFilter(source, event) #------------------------------------------------------------- def _show_stim_options(self): """ Hide / show panel with stimulus options """ self.wdg_ctrl_stim.setVisible(self.chk_stim_options.isChecked()) def _enable_stim_widgets(self): """ Enable / disable widgets depending on the selected stimulus""" self.stim = qget_cmb_box(self.cmbStimulus, data=False) f1_en = self.stim in {"Cos","Sine","Chirp","PM / FM","AM","Formula","Rect","Saw","Triang","Comb"} f2_en = self.stim in {"Cos","Sine","Chirp","PM / FM","AM","Formula"} dc_en = self.stim not in {"Step", "StepErr"} self.chk_stim_bl.setVisible(self.stim in {"Triang", "Saw", "Rect"}) self.lblAmp1.setVisible(self.stim != "None") self.ledAmp1.setVisible(self.stim != "None") self.chk_scale_impz_f.setVisible(self.stim == 'Impulse') self.chk_scale_impz_f.setEnabled((self.noi == 0 or self.cmbNoise.currentText() == 'None')\ and self.DC == 0) self.cmbChirpMethod.setVisible(self.stim == 'Chirp') self.lblPhi1.setVisible(f1_en) self.ledPhi1.setVisible(f1_en) self.lblPhU1.setVisible(f1_en) self.lblFreq1.setVisible(f1_en) self.ledFreq1.setVisible(f1_en) self.lblFreqUnit1.setVisible(f1_en) self.lblFreq2.setVisible(f2_en) self.ledFreq2.setVisible(f2_en) self.lblFreqUnit2.setVisible(f2_en) self.lblAmp2.setVisible(f2_en and self.stim != "Chirp") self.ledAmp2.setVisible(f2_en and self.stim != "Chirp") self.lblPhi2.setVisible(f2_en and self.stim != "Chirp") self.ledPhi2.setVisible(f2_en and self.stim != "Chirp") self.lblPhU2.setVisible(f2_en and self.stim != "Chirp") self.lblDC.setVisible(dc_en) self.ledDC.setVisible(dc_en) self.lblStimFormula.setVisible(self.stim == "Formula") self.ledStimFormula.setVisible(self.stim == "Formula") self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim'}) #------------------------------------------------------------- def load_fs(self): """ Reload sampling frequency from filter dictionary and transform the displayed frequency spec input fields according to the units setting (i.e. f_S). Spec entries are always stored normalized w.r.t. f_S in the dictionary; when f_S or the unit are changed, only the displayed values of the frequency entries are updated, not the dictionary! load_fs() is called during init and when the frequency unit or the sampling frequency have been changed. It should be called when sigSpecsChanged or sigFilterDesigned is emitted at another place, indicating that a reload is required. """ # recalculate displayed freq spec values for (maybe) changed f_S if self.ledFreq1.hasFocus(): # widget has focus, show full precision self.ledFreq1.setText(str(self.f1 * fb.fil[0]['f_S'])) elif self.ledFreq2.hasFocus(): # widget has focus, show full precision self.ledFreq2.setText(str(self.f2 * fb.fil[0]['f_S'])) else: # widgets have no focus, round the display self.ledFreq1.setText( str(params['FMT'].format(self.f1 * fb.fil[0]['f_S']))) self.ledFreq2.setText( str(params['FMT'].format(self.f2 * fb.fil[0]['f_S']))) def _update_amp1(self): """ Update value for self.A1 from QLineEditWidget""" self.A1 = safe_eval(self.ledAmp1.text(), self.A1, return_type='cmplx') self.ledAmp1.setText(str(self.A1)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'a1'}) def _update_amp2(self): """ Update value for self.A2 from the QLineEditWidget""" self.A2 = safe_eval(self.ledAmp2.text(), self.A2, return_type='cmplx') self.ledAmp2.setText(str(self.A2)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'a2'}) def _update_phi1(self): """ Update value for self.phi1 from QLineEditWidget""" self.phi1 = safe_eval(self.ledPhi1.text(), self.phi1, return_type='float') self.ledPhi1.setText(str(self.phi1)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'phi1'}) def _update_phi2(self): """ Update value for self.phi2 from the QLineEditWidget""" self.phi2 = safe_eval(self.ledPhi2.text(), self.phi2, return_type='float') self.ledPhi2.setText(str(self.phi2)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'phi2'}) def _update_chirp_method(self): """ Update value for self.chirp_method from the QLineEditWidget""" self.chirp_method = qget_cmb_box(self.cmbChirpMethod) # read current data string self.sig_tx.emit({'sender':__name__, 'ui_changed':'chirp_method'}) def _update_noi(self): """ Update type + value + label for self.noi for noise""" self.noise = qget_cmb_box(self.cmbNoise, data=False).lower() self.lblNoi.setVisible(self.noise!='none') self.ledNoi.setVisible(self.noise!='none') if self.noise!='none': self.noi = safe_eval(self.ledNoi.text(), 0, return_type='cmplx') self.ledNoi.setText(str(self.noi)) if self.noise == 'gauss': self.lblNoi.setText(to_html(" σ =", frmt='bi')) self.ledNoi.setToolTip("<span>Standard deviation of statistical process," "noise power is <i>P</i> = σ<sup>2</sup></span>") elif self.noise == 'uniform': self.lblNoi.setText(to_html(" Δ =", frmt='bi')) self.ledNoi.setToolTip("<span>Interval size for uniformly distributed process " "(e.g. quantization step size for quantization noise), " "centered around 0. Noise power is " "<i>P</i> = Δ<sup>2</sup>/12.</span>") elif self.noise == 'prbs': self.lblNoi.setText(to_html(" A =", frmt='bi')) self.ledNoi.setToolTip("<span>Amplitude of bipolar Pseudorandom Binary Sequence. " "Noise power is <i>P</i> = A<sup>2</sup>.</span>") self.sig_tx.emit({'sender':__name__, 'ui_changed':'noi'}) def _update_DC(self): """ Update value for self.DC from the QLineEditWidget""" self.DC = safe_eval(self.ledDC.text(), 0, return_type='cmplx') self.ledDC.setText(str(self.DC)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'dc'}) def _update_stim_formula(self): """Update string with formula to be evaluated by numexpr""" self.stim_formula = self.ledStimFormula.text().strip() self.ledStimFormula.setText(str(self.stim_formula)) self.sig_tx.emit({'sender':__name__, 'ui_changed':'stim_formula'}) # ------------------------------------------------------------------------- def update_N(self, emit=True): # called directly from impz or locally # between local triggering and updates upstream """ Update values for self.N and self.N_start from the QLineEditWidget, update the window and fire "ui_changed" """ if not isinstance(emit, bool): logger.error("update N: emit={0}".format(emit)) self.N_start = safe_eval(self.led_N_start.text(), self.N_start, return_type='int', sign='poszero') self.led_N_start.setText(str(self.N_start)) # update widget self.N_user = safe_eval(self.led_N_points.text(), self.N_user, return_type='int', sign='poszero') if self.N_user == 0: # automatic calculation self.N = self.calc_n_points(self.N_user) # widget remains set to 0 self.led_N_points.setText("0") # update widget else: self.N = self.N_user self.led_N_points.setText(str(self.N)) # update widget self.N_end = self.N + self.N_start # total number of points to be calculated: N + N_start # FFT window needs to be updated due to changed number of data points self._update_win_fft(emit=False) # don't emit anything here if emit: self.sig_tx.emit({'sender':__name__, 'ui_changed':'N'}) def _read_param1(self): """Read out textbox when editing is finished and update dict and fft window""" param = safe_eval(self.ledWinPar1.text(), self.win_dict['par'][0]['val'], return_type='float') if param < self.win_dict['par'][0]['min']: param = self.win_dict['par'][0]['min'] elif param > self.win_dict['par'][0]['max']: param = self.win_dict['par'][0]['max'] self.ledWinPar1.setText(str(param)) self.win_dict['par'][0]['val'] = param self._update_win_fft() def _read_param2(self): """Read out textbox when editing is finished and update dict and fft window""" param = safe_eval(self.ledWinPar2.text(), self.win_dict['par'][1]['val'], return_type='float') if param < self.win_dict['par'][1]['min']: param = self.win_dict['par'][1]['min'] elif param > self.win_dict['par'][1]['max']: param = self.win_dict['par'][1]['max'] self.ledWinPar2.setText(str(param)) self.win_dict['par'][1]['val'] = param self._update_win_fft() #------------------------------------------------------------------------------ def _update_win_fft(self, arg=None, emit=True): """ Update window type for FFT with different arguments: - signal-slot connection to combo-box -> index (int), absorbed by `arg` emit is not set -> emit=True - called by _read_param() -> empty -> emit=True - called by update_N(emit=False) """ if not isinstance(emit, bool): logger.error("update win: emit={0}".format(emit)) self.window_name = qget_cmb_box(self.cmb_win_fft, data=False) self.win = calc_window_function(self.win_dict, self.window_name, N=self.N, sym=False) n_par = self.win_dict['n_par'] self.lblWinPar1.setVisible(n_par > 0) self.ledWinPar1.setVisible(n_par > 0) self.lblWinPar2.setVisible(n_par > 1) self.ledWinPar2.setVisible(n_par > 1) if n_par > 0: self.lblWinPar1.setText(to_html(self.win_dict['par'][0]['name'] + " =", frmt='bi')) self.ledWinPar1.setText(str(self.win_dict['par'][0]['val'])) self.ledWinPar1.setToolTip(self.win_dict['par'][0]['tooltip']) if n_par > 1: self.lblWinPar2.setText(to_html(self.win_dict['par'][1]['name'] + " =", frmt='bi')) self.ledWinPar2.setText(str(self.win_dict['par'][1]['val'])) self.ledWinPar2.setToolTip(self.win_dict['par'][1]['tooltip']) self.nenbw = self.N * np.sum(np.square(self.win)) / (np.square(np.sum(self.win))) self.cgain = np.sum(self.win) / self.N # coherent gain self.win /= self.cgain # correct gain for periodic signals # only emit a signal for local triggers to prevent infinite loop: # - signal-slot connection passes a bool or an integer # - local function calls don't pass anything if emit is True: self.sig_tx.emit({'sender':__name__, 'ui_changed':'win'}) # ... but always notify the FFT widget via sig_tx_fft self.sig_tx_fft.emit({'sender':__name__, 'view_changed':'win'}) #------------------------------------------------------------------------------ def show_fft_win(self): """ Pop-up FFT window """ if self.but_fft_win.isChecked(): qstyle_widget(self.but_fft_win, "changed") else: qstyle_widget(self.but_fft_win, "normal") if self.fft_window is None: # no handle to the window? Create a new instance if self.but_fft_win.isChecked(): # Important: Handle to window must be class attribute otherwise it # (and the attached window) is deleted immediately when it goes out of scope self.fft_window = Plot_FFT_win(self, win_dict=self.win_dict, sym=False, title="pyFDA Spectral Window Viewer") self.sig_tx_fft.connect(self.fft_window.sig_rx) self.fft_window.sig_tx.connect(self.close_fft_win) self.fft_window.show() # modeless i.e. non-blocking popup window else: if not self.but_fft_win.isChecked(): if self.fft_window is None: logger.warning("FFT window is already closed!") else: self.fft_window.close() def close_fft_win(self): self.fft_window = None self.but_fft_win.setChecked(False) qstyle_widget(self.but_fft_win, "normal") #------------------------------------------------------------------------------ def calc_n_points(self, N_user = 0): """ Calculate number of points to be displayed, depending on type of filter (FIR, IIR) and user input. If the user selects 0 points, the number is calculated automatically. An improvement would be to calculate the dominant pole and the corresponding settling time. """ if N_user == 0: # set number of data points automatically if fb.fil[0]['ft'] == 'IIR': N = 100 else: N = min(len(fb.fil[0]['ba'][0]),100) # FIR: N = number of coefficients (max. 100) else: N = N_user return N
class Firwin(QWidget): FRMT = 'ba' # output format(s) of filter design routines 'zpk' / 'ba' / 'sos' # currently, only 'ba' is supported for firwin routines sig_tx = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.ft = 'FIR' self.fft_window = None # dictionary for firwin window settings self.win_dict = fb.fil[0]['win_fir'] c = Common() self.rt_dict = c.rt_base_iir self.rt_dict_add = { 'COM': { 'min': { 'msg': ('a', r"<br /><b>Note:</b> Filter order is only a rough approximation " "and most likely far too low!") }, 'man': { 'msg': ('a', r"Enter desired filter order <b><i>N</i></b> and " "<b>-6 dB</b> pass band corner " "frequency(ies) <b><i>F<sub>C</sub></i></b> .") }, }, 'LP': { 'man': {}, 'min': {} }, 'HP': { 'man': { 'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!") }, 'min': {} }, 'BS': { 'man': { 'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!") }, 'min': {} }, 'BP': { 'man': {}, 'min': {} }, } self.info = """**Windowed FIR filters** are designed by truncating the infinite impulse response of an ideal filter with a window function. The kind of used window has strong influence on ripple etc. of the resulting filter. **Design routines:** ``scipy.signal.firwin()`` """ #self.info_doc = [] is set in self._update_UI() #------------------- end of static info for filter tree --------------- #---------------------------------------------------------------------- def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter object, fb.filObj . """ # Combobox for selecting the algorithm to estimate minimum filter order self.cmb_firwin_alg = QComboBox(self) self.cmb_firwin_alg.setObjectName('wdg_cmb_firwin_alg') self.cmb_firwin_alg.addItems(['ichige', 'kaiser', 'herrmann']) # Minimum size, can be changed in the upper hierarchy levels using layouts: self.cmb_firwin_alg.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmb_firwin_alg.hide() # Combobox for selecting the window used for filter design self.cmb_firwin_win = QComboBox(self) self.cmb_firwin_win.addItems(get_window_names()) self.cmb_firwin_win.setObjectName('wdg_cmb_firwin_win') # Minimum size, can be changed in the upper hierarchy levels using layouts: self.cmb_firwin_win.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.but_fft_win = QPushButton(self) self.but_fft_win.setText("WIN FFT") self.but_fft_win.setToolTip( "Show time and frequency response of FFT Window") self.but_fft_win.setCheckable(True) self.but_fft_win.setChecked(False) self.lblWinPar1 = QLabel("a", self) self.lblWinPar1.setObjectName('wdg_lbl_firwin_1') self.ledWinPar1 = QLineEdit(self) self.ledWinPar1.setText("0.5") self.ledWinPar1.setObjectName('wdg_led_firwin_1') self.lblWinPar1.setVisible(False) self.ledWinPar1.setVisible(False) self.lblWinPar2 = QLabel("b", self) self.lblWinPar2.setObjectName('wdg_lbl_firwin_2') self.ledWinPar2 = QLineEdit(self) self.ledWinPar2.setText("0.5") self.ledWinPar2.setObjectName('wdg_led_firwin_2') self.ledWinPar2.setVisible(False) self.lblWinPar2.setVisible(False) self.layHWin1 = QHBoxLayout() self.layHWin1.addWidget(self.cmb_firwin_win) self.layHWin1.addWidget(self.but_fft_win) self.layHWin1.addWidget(self.cmb_firwin_alg) self.layHWin2 = QHBoxLayout() self.layHWin2.addWidget(self.lblWinPar1) self.layHWin2.addWidget(self.ledWinPar1) self.layHWin2.addWidget(self.lblWinPar2) self.layHWin2.addWidget(self.ledWinPar2) self.layVWin = QVBoxLayout() self.layVWin.addLayout(self.layHWin1) self.layVWin.addLayout(self.layHWin2) self.layVWin.setContentsMargins(0, 0, 0, 0) # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits) self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layVWin) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmb_firwin_alg.activated.connect(self._update_win_fft) self.cmb_firwin_win.activated.connect(self._update_win_fft) self.ledWinPar1.editingFinished.connect(self._read_param1) self.ledWinPar2.editingFinished.connect(self._read_param2) self.but_fft_win.clicked.connect(self.show_fft_win) #---------------------------------------------------------------------- self._load_dict() # get initial / last setting from dictionary self._update_win_fft() #============================================================================= # Copied from impz() #============================================================================== def _read_param1(self): """Read out textbox when editing is finished and update dict and fft window""" param = safe_eval(self.ledWinPar1.text(), self.win_dict['par'][0]['val'], sign='pos', return_type='float') if param < self.win_dict['par'][0]['min']: param = self.win_dict['par'][0]['min'] elif param > self.win_dict['par'][0]['max']: param = self.win_dict['par'][0]['max'] self.ledWinPar1.setText(str(param)) self.win_dict['par'][0]['val'] = param self._update_win_fft() def _read_param2(self): """Read out textbox when editing is finished and update dict and fft window""" param = safe_eval(self.ledWinPar2.text(), self.win_dict['par'][1]['val'], return_type='float') if param < self.win_dict['par'][1]['min']: param = self.win_dict['par'][1]['min'] elif param > self.win_dict['par'][1]['max']: param = self.win_dict['par'][1]['max'] self.ledWinPar2.setText(str(param)) self.win_dict['par'][1]['val'] = param self._update_win_fft() def _update_win_fft(self): """ Update window type for FirWin """ self.alg = str(self.cmb_firwin_alg.currentText()) self.fir_window_name = qget_cmb_box(self.cmb_firwin_win, data=False) self.win = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) n_par = self.win_dict['n_par'] self.lblWinPar1.setVisible(n_par > 0) self.ledWinPar1.setVisible(n_par > 0) self.lblWinPar2.setVisible(n_par > 1) self.ledWinPar2.setVisible(n_par > 1) if n_par > 0: self.lblWinPar1.setText( to_html(self.win_dict['par'][0]['name'] + " =", frmt='bi')) self.ledWinPar1.setText(str(self.win_dict['par'][0]['val'])) self.ledWinPar1.setToolTip(self.win_dict['par'][0]['tooltip']) if n_par > 1: self.lblWinPar2.setText( to_html(self.win_dict['par'][1]['name'] + " =", frmt='bi')) self.ledWinPar2.setText(str(self.win_dict['par'][1]['val'])) self.ledWinPar2.setToolTip(self.win_dict['par'][1]['tooltip']) # sig_tx -> select_filter -> filter_specs self.sig_tx.emit({'sender': __name__, 'filt_changed': 'firwin'}) #============================================================================= def _load_dict(self): """ Reload window selection and parameters from filter dictionary and set UI elements accordingly. load_dict() is called upon initialization and when the filter is loaded from disk. """ self.N = fb.fil[0]['N'] win_idx = 0 alg_idx = 0 if 'wdg_fil' in fb.fil[0] and 'firwin' in fb.fil[0]['wdg_fil']: wdg_fil_par = fb.fil[0]['wdg_fil']['firwin'] if 'win' in wdg_fil_par: if np.isscalar( wdg_fil_par['win']): # true for strings (non-vectors) window = wdg_fil_par['win'] else: window = wdg_fil_par['win'][0] self.ledWinPar1.setText(str(wdg_fil_par['win'][1])) if len(wdg_fil_par['win']) > 2: self.ledWinPar2.setText(str(wdg_fil_par['win'][2])) # find index for window string win_idx = self.cmb_firwin_win.findText( window, Qt.MatchFixedString) # case insensitive flag if win_idx == -1: # Key does not exist, use first entry instead win_idx = 0 if 'alg' in wdg_fil_par: alg_idx = self.cmb_firwin_alg.findText(wdg_fil_par['alg'], Qt.MatchFixedString) if alg_idx == -1: # Key does not exist, use first entry instead alg_idx = 0 self.cmb_firwin_win.setCurrentIndex( win_idx) # set index for window and self.cmb_firwin_alg.setCurrentIndex(alg_idx) # and algorithm cmbBox def _store_entries(self): """ Store window and alg. selection and parameter settings (part of self.firWindow, if any) in filter dictionary. """ if not 'wdg_fil' in fb.fil[0]: fb.fil[0].update({'wdg_fil': {}}) fb.fil[0]['wdg_fil'].update( {'firwin': { 'win': self.firWindow, 'alg': self.alg }}) def _get_params(self, fil_dict): """ Translate parameters from the passed dictionary to instance parameters, scaling / transforming them if needed. """ self.N = fil_dict['N'] self.F_PB = fil_dict['F_PB'] self.F_SB = fil_dict['F_SB'] self.F_PB2 = fil_dict['F_PB2'] self.F_SB2 = fil_dict['F_SB2'] self.F_C = fil_dict['F_C'] self.F_C2 = fil_dict['F_C2'] # firwin amplitude specs are linear (not in dBs) self.A_PB = fil_dict['A_PB'] self.A_PB2 = fil_dict['A_PB2'] self.A_SB = fil_dict['A_SB'] self.A_SB2 = fil_dict['A_SB2'] # self.alg = 'ichige' # algorithm for determining the minimum order # self.alg = self.cmb_firwin_alg.currentText() def _test_N(self): """ Warn the user if the calculated order is too high for a reasonable filter design. """ if self.N > 1000: return qfilter_warning(self, self.N, "FirWin") else: return True def _save(self, fil_dict, arg): """ Convert between poles / zeros / gain, filter coefficients (polynomes) and second-order sections and store all available formats in the passed dictionary 'fil_dict'. """ fil_save(fil_dict, arg, self.FRMT, __name__) try: # has the order been calculated by a "min" filter design? fil_dict['N'] = self.N # yes, update filterbroker except AttributeError: pass # self._store_entries() #------------------------------------------------------------------------------ def firwin(self, numtaps, cutoff, window=None, pass_zero=True, scale=True, nyq=1.0, fs=None): """ FIR filter design using the window method. This is more or less the same as `scipy.signal.firwin` with the exception that an ndarray with the window values can be passed as an alternative to the window name. The parameters "width" (specifying a Kaiser window) and "fs" have been omitted, they are not needed here. This function computes the coefficients of a finite impulse response filter. The filter will have linear phase; it will be Type I if `numtaps` is odd and Type II if `numtaps` is even. Type II filters always have zero response at the Nyquist rate, so a ValueError exception is raised if firwin is called with `numtaps` even and having a passband whose right end is at the Nyquist rate. Parameters ---------- numtaps : int Length of the filter (number of coefficients, i.e. the filter order + 1). `numtaps` must be even if a passband includes the Nyquist frequency. cutoff : float or 1D array_like Cutoff frequency of filter (expressed in the same units as `nyq`) OR an array of cutoff frequencies (that is, band edges). In the latter case, the frequencies in `cutoff` should be positive and monotonically increasing between 0 and `nyq`. The values 0 and `nyq` must not be included in `cutoff`. window : ndarray or string string: use the window with the passed name from scipy.signal.windows ndarray: The window values - this is an addition to the original firwin routine. pass_zero : bool, optional If True, the gain at the frequency 0 (i.e. the "DC gain") is 1. Otherwise the DC gain is 0. scale : bool, optional Set to True to scale the coefficients so that the frequency response is exactly unity at a certain frequency. That frequency is either: - 0 (DC) if the first passband starts at 0 (i.e. pass_zero is True) - `nyq` (the Nyquist rate) if the first passband ends at `nyq` (i.e the filter is a single band highpass filter); center of first passband otherwise nyq : float, optional Nyquist frequency. Each frequency in `cutoff` must be between 0 and `nyq`. Returns ------- h : (numtaps,) ndarray Coefficients of length `numtaps` FIR filter. Raises ------ ValueError If any value in `cutoff` is less than or equal to 0 or greater than or equal to `nyq`, if the values in `cutoff` are not strictly monotonically increasing, or if `numtaps` is even but a passband includes the Nyquist frequency. See also -------- scipy.firwin """ cutoff = np.atleast_1d(cutoff) / float(nyq) # Check for invalid input. if cutoff.ndim > 1: raise ValueError("The cutoff argument must be at most " "one-dimensional.") if cutoff.size == 0: raise ValueError("At least one cutoff frequency must be given.") if cutoff.min() <= 0 or cutoff.max() >= 1: raise ValueError( "Invalid cutoff frequency {0}: frequencies must be " "greater than 0 and less than nyq.".format(cutoff)) if np.any(np.diff(cutoff) <= 0): raise ValueError("Invalid cutoff frequencies: the frequencies " "must be strictly increasing.") pass_nyquist = bool(cutoff.size & 1) ^ pass_zero if pass_nyquist and numtaps % 2 == 0: raise ValueError( "A filter with an even number of coefficients must " "have zero response at the Nyquist rate.") # Insert 0 and/or 1 at the ends of cutoff so that the length of cutoff # is even, and each pair in cutoff corresponds to passband. cutoff = np.hstack(([0.0] * pass_zero, cutoff, [1.0] * pass_nyquist)) # `bands` is a 2D array; each row gives the left and right edges of # a passband. bands = cutoff.reshape(-1, 2) # Build up the coefficients. alpha = 0.5 * (numtaps - 1) m = np.arange(0, numtaps) - alpha h = 0 for left, right in bands: h += right * sinc(right * m) h -= left * sinc(left * m) if type(window) == str: # Get and apply the window function. from scipy.signal.signaltools import get_window win = get_window(window, numtaps, fftbins=False) elif type(window) == np.ndarray: win = window else: logger.error( "The 'window' was neither a string nor a numpy array, it could not be evaluated." ) return None # apply the window function. h *= win # Now handle scaling if desired. if scale: # Get the first passband. left, right = bands[0] if left == 0: scale_frequency = 0.0 elif right == 1: scale_frequency = 1.0 else: scale_frequency = 0.5 * (left + right) c = np.cos(np.pi * m * scale_frequency) s = np.sum(h * c) h /= s return h def _firwin_ord(self, F, W, A, alg): #http://www.mikroe.com/chapters/view/72/chapter-2-fir-filters/ delta_f = abs(F[1] - F[0]) * 2 # referred to f_Ny delta_A = np.sqrt(A[0] * A[1]) if self.fir_window_name == 'kaiser': N, beta = sig.kaiserord(20 * np.log10(np.abs(fb.fil[0]['A_SB'])), delta_f) self.ledWinPar1.setText(str(beta)) fb.fil[0]['wdg_fil'][1] = beta self._update_UI() else: N = remezord(F, W, A, fs=1, alg=alg)[0] return N def LPmin(self, fil_dict): self._get_params(fil_dict) self.N = self._firwin_ord([self.F_PB, self.F_SB], [1, 0], [self.A_PB, self.A_SB], alg=self.alg) if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) fil_dict['F_C'] = (self.F_SB + self.F_PB ) / 2 # use average of calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], window=self.fir_window, nyq=0.5)) def LPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], window=self.fir_window, nyq=0.5)) def HPmin(self, fil_dict): self._get_params(fil_dict) N = self._firwin_ord([self.F_SB, self.F_PB], [0, 1], [self.A_SB, self.A_PB], alg=self.alg) self.N = round_odd(N) # enforce odd order if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) fil_dict['F_C'] = (self.F_SB + self.F_PB ) / 2 # use average of calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], window=self.fir_window, pass_zero=False, nyq=0.5)) def HPman(self, fil_dict): self._get_params(fil_dict) self.N = round_odd(self.N) # enforce odd order if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], window=self.fir_window, pass_zero=False, nyq=0.5)) # For BP and BS, F_PB and F_SB have two elements each def BPmin(self, fil_dict): self._get_params(fil_dict) self.N = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2], [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2], fs=1, alg=self.alg)[0] if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) fil_dict['F_C'] = (self.F_SB + self.F_PB ) / 2 # use average of calculated F_PB and F_SB fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2 ) / 2 # use average of calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.fir_window, pass_zero=False, nyq=0.5)) def BPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.fir_window, pass_zero=False, nyq=0.5)) def BSmin(self, fil_dict): self._get_params(fil_dict) N = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2], [1, 0, 1], [self.A_PB, self.A_SB, self.A_PB2], fs=1, alg=self.alg)[0] self.N = round_odd(N) # enforce odd order if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) fil_dict['F_C'] = (self.F_SB + self.F_PB ) / 2 # use average of calculated F_PB and F_SB fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2 ) / 2 # use average of calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.fir_window, pass_zero=True, nyq=0.5)) def BSman(self, fil_dict): self._get_params(fil_dict) self.N = round_odd(self.N) # enforce odd order if not self._test_N(): return -1 self.fir_window = calc_window_function(self.win_dict, self.fir_window_name, N=self.N, sym=True) self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.fir_window, pass_zero=True, nyq=0.5)) #------------------------------------------------------------------------------ def show_fft_win(self): """ Pop-up FFT window """ if self.but_fft_win.isChecked(): qstyle_widget(self.but_fft_win, "changed") else: qstyle_widget(self.but_fft_win, "normal") if self.fft_window is None: # no handle to the window? Create a new instance if self.but_fft_win.isChecked(): # important: Handle to window must be class attribute # pass the name of the dictionary where parameters are stored and # whether a symmetric window or one that can be continued periodically # will be constructed self.fft_window = Plot_FFT_win(self, win_dict=self.win_dict, sym=True, title="pyFDA FIR Window Viewer") self.sig_tx.connect(self.fft_window.sig_rx) self.fft_window.sig_tx.connect(self.close_fft_win) self.fft_window.show( ) # modeless i.e. non-blocking popup window else: if not self.but_fft_win.isChecked(): if self.fft_window is None: logger.warning("FFT window is already closed!") else: self.fft_window.close() def close_fft_win(self): self.fft_window = None self.but_fft_win.setChecked(False) qstyle_widget(self.but_fft_win, "normal")
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_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)
class AllpPZ(QWidget): FRMT = 'zpk' # output format of delay filter widget info = """ **Allpass widget** allows entering the two **poles** :math:`p`. **zeros** are calculated from the reciprocal values of the poles. There is no minimum algorithm, only the two poles can be entered manually. """ sig_tx = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.p = [0.5, 0.5j] self.ft = 'IIR' # the following defines which subwidgets are "a"ctive, "i"nvisible or "d"eactivated self.rt_dicts = ('com', ) self.rt_dict = { 'COM': { 'man': { 'fo': ('d', 'N'), 'msg': ('a', "<span>Enter poles <b><i>p</i></b> for allpass function," "zeros will be calculated.</span>") }, }, 'AP': { 'man': {} } } self.info_doc = [] #-------------------------------------------------------------------------- def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter instance, fb.fil_inst. """ self.lbl_pole1 = QLabel("Pole 1", self) self.lbl_pole1.setObjectName('wdg_lbl_pole1') self.led_pole1 = QLineEdit(self) self.led_pole1.setText(str(self.p[0])) self.led_pole1.setObjectName('wdg_led_pole1') self.led_pole1.setToolTip("Pole 1 for allpass filter") self.lbl_pole2 = QLabel("Pole 2", self) self.lbl_pole2.setObjectName('wdg_lbl_pole2') self.led_pole2 = QLineEdit(self) self.led_pole2.setText(str(self.p[1])) self.led_pole2.setObjectName('wdg_led_pole2') self.led_pole2.setToolTip("Pole 2 for allpass filter") self.layHWin = QHBoxLayout() self.layHWin.setObjectName('wdg_layGWin') self.layHWin.addWidget(self.lbl_pole1) self.layHWin.addWidget(self.led_pole1) self.layHWin.addWidget(self.lbl_pole2) self.layHWin.addWidget(self.led_pole2) self.layHWin.setContentsMargins(0, 0, 0, 0) # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits) self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layHWin) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.led_pole1.editingFinished.connect(self._update_UI) self.led_pole2.editingFinished.connect(self._update_UI) # fires when edited line looses focus or when RETURN is pressed #---------------------------------------------------------------------- self._load_dict() # get initial / last setting from dictionary self._update_UI() def _update_UI(self): """ Update UI when line edit field is changed (here, only the text is read and converted to integer) and store parameter settings in filter dictionary """ self.p[0] = safe_eval(self.led_pole1.text(), self.p[0], return_type='cmplx') self.led_pole1.setText(str(self.p[0])) self.p[1] = safe_eval(self.led_pole2.text(), self.p[1], return_type='cmplx') self.led_pole2.setText(str(self.p[1])) if not 'wdg_fil' in fb.fil[0]: fb.fil[0].update({'wdg_fil': {}}) fb.fil[0]['wdg_fil'].update( {'poles': { 'p1': self.p[0], 'p2': self.p[1] }}) # sig_tx -> select_filter -> filter_specs self.sig_tx.emit({'sender': __name__, 'filt_changed': 'pole_1_2'}) def _load_dict(self): """ Reload parameter(s) from filter dictionary (if they exist) and set corresponding UI elements. _load_dict() is called upon initialization and when the filter is loaded from disk. """ if 'wdg_fil' in fb.fil[0] and 'poles' in fb.fil[0]['wdg_fil']: wdg_fil_par = fb.fil[0]['wdg_fil']['poles'] if 'p1' in wdg_fil_par: self.p1 = wdg_fil_par['p1'] self.led_pole1.setText(str(self.p1)) if 'p2' in wdg_fil_par: self.p2 = wdg_fil_par['p2'] self.led_pole2.setText(str(self.p2)) def _get_params(self, fil_dict): """ Get parameters needed for filter design from the passed dictionary and translate them to instance parameters, scaling / transforming them if needed. """ #self.p1 = fil_dict['zpk'][1][0] # get the first and second pole #self.p2 = fil_dict['zpk'][1][1] # from central filter dect logger.info(fil_dict['zpk']) def _test_poles(self): """ Warn the user if one of the poles is outside the unit circle """ if abs(self.p[0]) >= 1 or abs(self.p[1]) >= 1: return qfilter_warning(self, self.p[0], "Delay") else: return True def _save(self, fil_dict, arg=None): """ Convert between poles / zeros / gain, filter coefficients (polynomes) and second-order sections and store all available formats in the passed dictionary 'fil_dict'. """ if arg is None: logger.error("Passed empty filter dict") logger.info(arg) fil_save(fil_dict, arg, self.FRMT, __name__) fil_dict['N'] = len(self.p) #------------------------------------------------------------------------------ # Filter design routines #------------------------------------------------------------------------------ # The method name MUST be "FilterType"+"MinMan", e.g. LPmin or BPman def APman(self, fil_dict): """ Calculate z =1/p* for a given set of poles p. If p=0, set z=0. The gain factor k is calculated from z and p at z = 1. """ self._get_params(fil_dict) # not needed here if not self._test_poles(): return -1 self.z = [0, 0] if self.p[0] != 0: self.z[0] = np.conj(1 / self.p[0]) if type(self.p[0]) == complex: pass if self.p[1] != 0: self.z[1] = np.conj(1 / self.p[1]) k = np.abs( np.polyval(np.poly(self.p), 1) / np.polyval(np.poly(self.z), 1)) zpk_list = [self.z, self.p, k] self._save(fil_dict, zpk_list)
class PlotTabWidgets(QTabWidget): # incoming, connected to input_tab_widget.sig_tx in pyfdax sig_rx = pyqtSignal(object) # outgoing: emitted by process_sig_rx sig_tx = pyqtSignal(object) def __init__(self, parent): super(PlotTabWidgets, self).__init__(parent) self._construct_UI() #---------------------------------------------- -------------------------------- def _construct_UI(self): """ Initialize UI with tabbed subwidgets: Instantiate dynamically each widget from the dict `fb.plot_classes` and try to - set the TabToolTip from the instance attribute `tool_tip` - set the tab label from the instance attribute `tab_label` for each widget. - connect the available signals of all subwidgets (not all widgets have both `sig_rx` and `sig_tx` signals). - `self.sig_rx` is distributed to all `inst.sig_rx` signals - all `inst.sig_tx` signals are collected in `self.sig_tx` - `self.sig_tx.connect(self.sig_rx)` distributes incoming signals (via pyfdax or coming from the input widgets) among all input widgets. In order to prevent infinite loops, every widget needs to block in- coming signals with its own name! """ tabWidget = QTabWidget(self) tabWidget.setObjectName("plot_tabs") n_wdg = 0 # number and ... inst_wdg_str = "" # ... full names of successfully instantiated plot widgets # for plot_class in fb.plot_classes: try: mod_fq_name = fb.plot_classes[plot_class][ 'mod'] # fully qualified module name mod = importlib.import_module( mod_fq_name) # import plot widget module wdg_class = getattr(mod, plot_class) # get plot widget class ... # and instantiate it inst = wdg_class(self) except ImportError as e: logger.warning('Class "{0}" could not be imported from {1}:\n{2}.'\ .format(plot_class, mod_fq_name, e)) continue # unsuccessful, try next widget if hasattr(inst, 'tab_label'): tabWidget.addTab(inst, inst.tab_label) else: tabWidget.addTab(inst, "not set") if hasattr(inst, 'tool_tip'): tabWidget.setTabToolTip(n_wdg, inst.tool_tip) if hasattr(inst, 'sig_tx'): inst.sig_tx.connect(self.sig_tx) if hasattr(inst, 'sig_rx'): self.sig_rx.connect(inst.sig_rx) n_wdg += 1 # successfully instantiated one more widget inst_wdg_str += '\t' + mod_fq_name + "." + plot_class + '\n' if len(inst_wdg_str) == 0: logger.warning("No plotting widgets found!") else: logger.debug("Imported {0:d} plotting classes:\n{1}".format( n_wdg, inst_wdg_str)) #---------------------------------------------------------------------- layVMain = QVBoxLayout() layVMain.addWidget(tabWidget) layVMain.setContentsMargins( *params['wdg_margins']) #(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- # self.sig_rx.connect(inst.sig_rx) # this happens in _construct_UI() #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.timer_id = QtCore.QTimer() self.timer_id.setSingleShot(True) # redraw current widget at timeout (timer was triggered by resize event): self.timer_id.timeout.connect(self.current_tab_redraw) self.sig_tx.connect(self.sig_rx) # loop back to local inputs # self.sig_rx.connect(self.log_rx) # enable for debugging # When user has selected a different tab, trigger a redraw of current tab tabWidget.currentChanged.connect(self.current_tab_changed) # The following does not work: maybe current scope must be left? # tabWidget.currentChanged.connect(tabWidget.currentWidget().redraw) tabWidget.installEventFilter(self) """ https://stackoverflow.com/questions/29128936/qtabwidget-size-depending-on-current-tab The QTabWidget won't select the biggest widget's height as its own height unless you use layout on the QTabWidget. Therefore, if you want to change the size of QTabWidget manually, remove the layout and call QTabWidget::resize according to the currentChanged signal. You can set the size policy of the widget that is displayed to QSizePolicy::Preferred and the other ones to QSizePolicy::Ignored. After that call adjustSize to update the sizes. void MainWindow::updateSizes(int index) { for(int i=0;i<ui->tabWidget->count();i++) if(i!=index) ui->tabWidget->widget(i)->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); ui->tabWidget->widget(index)->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); ui->tabWidget->widget(index)->resize(ui->tabWidget->widget(index)->minimumSizeHint()); ui->tabWidget->widget(index)->adjustSize(); resize(minimumSizeHint()); adjustSize(); } adjustSize(): The last two lines resize the main window itself. You might want to avoid it, depending on your application. For example, if you set the rest of the widgets to expand into the space just made available, it's not so nice if the window resizes itself instead. """ #------------------------------------------------------------------------------ def log_rx(self, dict_sig=None): """ Enable `self.sig_rx.connect(self.log_rx)` above for debugging. """ if type(dict_sig) == dict: logger.warning("SIG_RX\n{0}"\ .format(pprint_log(dict_sig))) else: logger.warning("empty dict") #------------------------------------------------------------------------------ def current_tab_changed(self): self.sig_tx.emit({'sender': __name__, 'ui_changed': 'tab'}) #------------------------------------------------------------------------------ def current_tab_redraw(self): self.sig_tx.emit({'sender': __name__, 'ui_changed': 'resized'}) #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QTabWidget. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. This filter stops and restarts a one-shot timer for every resize event. When the timer generates a timeout after 500 ms, ``current_tab_redraw()`` is called by the timer. """ if isinstance(source, QTabWidget): if event.type() == QEvent.Resize: self.timer_id.stop() self.timer_id.start(500) # Call base class method to continue normal event processing: return super(PlotTabWidgets, self).eventFilter(source, event)
class EllipZeroPhz(QWidget): # Since we are also using poles/residues -> let's force zpk # if SOS_AVAIL: # output format of filter design routines 'zpk' / 'ba' / 'sos' # FRMT = 'sos' # else: FRMT = 'zpk' info = """ **Elliptic filters with zero phase** (also known as Cauer filters) have the steepest rate of transition between the frequency response’s passband and stopband of all IIR filters. This comes at the expense of a constant ripple (equiripple) :math:`A_PB` and :math:`A_SB` in both pass and stop band. Ringing of the step response is increased in comparison to Chebychev filters. As the passband ripple :math:`A_PB` approaches 0, the elliptical filter becomes a Chebyshev type II filter. As the stopband ripple :math:`A_SB` approaches 0, it becomes a Chebyshev type I filter. As both approach 0, becomes a Butterworth filter (butter). For the filter design, the order :math:`N`, minimum stopband attenuation :math:`A_SB` and the critical frequency / frequencies :math:`F_PB` where the gain first drops below the maximum passband ripple :math:`-A_PB` have to be specified. The ``ellipord()`` helper routine calculates the minimum order :math:`N` and critical passband frequency :math:`F_C` from pass and stop band specifications. The Zero Phase Elliptic Filter squares an elliptic filter designed in a way to produce the required Amplitude specifications. So initially the amplitude specs design an elliptic filter with the square root of the amp specs. The filter is then squared to produce a zero phase filter. The filter coefficients are applied to the signal data in a backward and forward time fashion. This filter can only be applied to stored signal data (not real-time streaming data that comes in a forward time order). We are forcing the order N of the filter to be even. This simplifies the poles/zeros to be complex (no real values). **Design routines:** ``scipy.signal.ellip()``, ``scipy.signal.ellipord()`` """ sig_tx = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.ft = 'IIR' c = Common() self.rt_dict = c.rt_base_iir self.rt_dict_add = { 'COM':{'man':{'msg':('a', "Enter the filter order <b><i>N</i></b>, the minimum stop " "band attenuation <b><i>A<sub>SB</sub></i></b> and frequency or " "frequencies <b><i>F<sub>C</sub></i></b> where gain first drops " "below the max passband ripple <b><i>-A<sub>PB</sub></i></b> .")}}, 'LP': {'man':{}, 'min':{}}, 'HP': {'man':{}, 'min':{}}, 'BS': {'man':{}, 'min':{}}, 'BP': {'man':{}, 'min':{}}, } self.info_doc = [] self.info_doc.append('ellip()\n========') self.info_doc.append(sig.ellip.__doc__) self.info_doc.append('ellipord()\n==========') self.info_doc.append(ellipord.__doc__) #-------------------------------------------------------------------------- def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter instance, fb.fil_inst. """ self.chkComplex = QCheckBox("ComplexFilter", self) self.chkComplex.setToolTip("Designs BP or BS Filter for complex data.") self.chkComplex.setObjectName('wdg_lbl_el') self.chkComplex.setChecked(False) #-------------------------------------------------- # Layout for filter optional subwidgets self.layHWin = QHBoxLayout() self.layHWin.setObjectName('wdg_layGWin') #self.layHWin.addWidget(self.chkComplex) self.layHWin.addStretch() self.layHWin.setContentsMargins(0,0,0,0) # Widget containing all subwidgets self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layHWin) def _get_params(self, fil_dict): """ Translate parameters from the passed dictionary to instance parameters, scaling / transforming them if needed. For zero phase filter, we take square root of amplitude specs since we later square filter. Define design around smallest amp spec """ # Frequencies are normalized to f_Nyq = f_S/2, ripple specs are in dB self.analog = False # set to True for analog filters self.manual = False # default is normal design self.N = int(fil_dict['N']) # force N to be even if (self.N % 2) == 1: self.N += 1 self.F_PB = fil_dict['F_PB'] * 2 self.F_SB = fil_dict['F_SB'] * 2 self.F_PB2 = fil_dict['F_PB2'] * 2 self.F_SB2 = fil_dict['F_SB2'] * 2 self.F_PBC = None # find smallest spec'd linear value and rewrite dictionary ampPB = fil_dict['A_PB'] ampSB = fil_dict['A_SB'] # take square roots of amp specs so resulting squared # filter will meet specifications if (ampPB < ampSB): ampSB = sqrt(ampPB) ampPB = sqrt(1+ampPB)-1 else: ampPB = sqrt(1+ampSB)-1 ampSB = sqrt(ampSB) self.A_PB = lin2unit(ampPB, 'IIR', 'A_PB', unit='dB') self.A_SB = lin2unit(ampSB, 'IIR', 'A_SB', unit='dB') #logger.warning("design with "+str(self.A_PB)+","+str(self.A_SB)) # ellip filter routines support only one amplitude spec for # pass- and stop band each if str(fil_dict['rt']) == 'BS': fil_dict['A_PB2'] = self.A_PB elif str(fil_dict['rt']) == 'BP': fil_dict['A_SB2'] = self.A_SB # partial fraction expansion to define residue vector def _partial(self, k, p, z, norder): # create diff array diff = p - z # now compute residual vector cone = complex(1.,0.) residues = zeros(norder, complex) for i in range(norder): residues[i] = k * (diff[i] / p[i]) for j in range(norder): if (j != i): residues[i] = residues[i] * (cone + diff[j]/(p[i] - p[j])) # now compute DC term for new expansion sumRes = 0. for i in range(norder): sumRes = sumRes + residues[i].real dc = k - sumRes return (dc, residues) # # Take a causal filter and square it. The result has the square # of the amplitude response of the input, and zero phase. Filter # is noncausal. # Input: # k - gain in pole/zero form # p - numpy array of poles # z - numpy array of zeros # g - gain in pole/residue form # r - numpy array of residues # nn- order of filter # Output: # kn - new gain (pole/zero) # pn - new poles # zn - new zeros (numpy array) # gn - new gain (pole/residue) # rn - new residues def _sqCausal (self, k, p, z, g, r, nn): # Anticausal poles have conjugate-reciprocal symmetry # Starting anticausal residues are conjugates (adjusted below) pA = conj(1./p) # antiCausal poles zA = conj(z) # antiCausal zeros (store reciprocal) rA = conj(r) # antiCausal residues (to start) rC = zeros(nn, complex) # Adjust residues. Causal part first. for j in range(nn): # Evaluate the anticausal filter at each causal pole tmpx = rA / (1. - p[j]/pA) ztmp = g + sum(tmpx) # Adjust residue rC[j] = r[j]*ztmp # anticausal residues are just conjugates of causal residues # r3 = np.conj(r2) # Compute the constant term dc2 = (g + sum(r))*g - sum(rC) # Populate output (2nn elements) gn = dc2.real # Drop complex poles/residues in LHP, keep only UHP pA = conj(p) #store AntiCasual pole (reciprocal) p0 = zeros(int(nn/2), complex) r0 = zeros(int(nn/2), complex) cnt = 0 for j in range(nn): if (p[j].imag > 0.0): p0[cnt] = p[j] r0[cnt] = rC[j] cnt = cnt+1 # Let operator know we squared filter # logger.info('After squaring filter, order: '+str(nn*2)) # For now and our case, only store causal residues # Filters are symmetric and can generate antiCausal residues return (pA, zA, gn, p0, r0) def _test_N(self): """ Warn the user if the calculated order is too high for a reasonable filter design. """ if self.N > 30: return qfilter_warning(self, self.N, "Zero-phase Elliptic") else: return True # custom save of filter dictionary def _save(self, fil_dict, arg): """ First design initial elliptic filter meeting sqRoot Amp specs; - Then create residue vector from poles/zeros; - Then square filter (k,p,z and dc,p,r) to get zero phase filter; - Then Convert results of filter design to all available formats (pz, pr, ba, sos) and store them in the global filter dictionary. Corner frequencies and order calculated for minimum filter order are also stored to allow for an easy subsequent manual filter optimization. """ fil_save(fil_dict, arg, self.FRMT, __name__) # For min. filter order algorithms, update filter dict with calculated # new values for filter order N and corner frequency(s) F_PBC fil_dict['N'] = self.N if str(fil_dict['fo']) == 'min': if str(fil_dict['rt']) == 'LP' or str(fil_dict['rt']) == 'HP': # HP or LP - single corner frequency fil_dict['F_PB'] = self.F_PBC / 2. else: # BP or BS - two corner frequencies fil_dict['F_PB'] = self.F_PBC[0] / 2. fil_dict['F_PB2'] = self.F_PBC[1] / 2. # Now generate poles/residues for custom file save of new parameters if (not self.manual): z = fil_dict['zpk'][0] p = fil_dict['zpk'][1] k = fil_dict['zpk'][2] n = len(z) gain, residues = self._partial (k, p, z, n) pA, zA, gn, pC, rC = self._sqCausal (k, p, z, gain, residues, n) fil_dict['rpk'] = [rC, pC, gn] # save antiCausal b,a (nonReciprocal) also [easier to compute h(n) try: fil_dict['baA'] = sig.zpk2tf(zA, pA, k) except Exception as e: logger.error(e) # 'rpk' is our signal that this is a non-Causal filter with zero phase # inserted into fil dictionary after fil_save and convert # sig_tx -> select_filter -> filter_specs self.sig_tx.emit({'sender':__name__, 'filt_changed':'ellip_zero'}) #------------------------------------------------------------------------------ # # DESIGN ROUTINES # #------------------------------------------------------------------------------ # LP: F_PB < F_stop ------------------------------------------------------- def LPmin(self, fil_dict): """Elliptic LP filter, minimum order""" self._get_params(fil_dict) self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB, analog=self.analog) # force even N if (self.N%2)== 1: self.N += 1 if not self._test_N(): return -1 #logger.warning("and "+str(self.F_PBC) + " " + str(self.N)) self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC, btype='low', analog=self.analog, output=self.FRMT)) def LPman(self, fil_dict): """Elliptic LP filter, manual order""" self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB, btype='low', analog=self.analog, output=self.FRMT)) # HP: F_stop < F_PB ------------------------------------------------------- def HPmin(self, fil_dict): """Elliptic HP filter, minimum order""" self._get_params(fil_dict) self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB, analog=self.analog) # force even N if (self.N%2)== 1: self.N += 1 if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC, btype='highpass', analog=self.analog, output=self.FRMT)) def HPman(self, fil_dict): """Elliptic HP filter, manual order""" self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB, btype='highpass', analog=self.analog, output=self.FRMT)) # For BP and BS, F_XX have two elements each, A_XX has only one # BP: F_SB[0] < F_PB[0], F_SB[1] > F_PB[1] -------------------------------- def BPmin(self, fil_dict): """Elliptic BP filter, minimum order""" self._get_params(fil_dict) self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2], [self.F_SB, self.F_SB2], self.A_PB, self.A_SB, analog=self.analog) #logger.warning(" "+str(self.F_PBC) + " " + str(self.N)) if (self.N%2)== 1: self.N += 1 if not self._test_N(): return -1 #logger.warning("-"+str(self.F_PBC) + " " + str(self.A_SB)) self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC, btype='bandpass', analog=self.analog, output=self.FRMT)) def BPman(self, fil_dict): """Elliptic BP filter, manual order""" self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, [self.F_PB,self.F_PB2], btype='bandpass', analog=self.analog, output=self.FRMT)) # BS: F_SB[0] > F_PB[0], F_SB[1] < F_PB[1] -------------------------------- def BSmin(self, fil_dict): """Elliptic BP filter, minimum order""" self._get_params(fil_dict) self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2], [self.F_SB, self.F_SB2], self.A_PB,self.A_SB, analog=self.analog) # force even N if (self.N%2)== 1: self.N += 1 if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC, btype='bandstop', analog=self.analog, output=self.FRMT)) def BSman(self, fil_dict): """Elliptic BS filter, manual order""" self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, [self.F_PB,self.F_PB2], btype='bandstop', analog=self.analog, output=self.FRMT))
class AmplitudeSpecs(QWidget): """ Build and update widget for entering the amplitude specifications like A_SB, A_PB etc. """ sig_tx = pyqtSignal( object) # emitted when amplitude unit or spec has been changed def __init__(self, parent, title="Amplitude Specs"): """ Initialize """ super(AmplitudeSpecs, self).__init__(parent) self.title = title self.qlabels = [] # list with references to QLabel widgets self.qlineedit = [] # list with references to QLineEdit widgets self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() #------------------------------------------------------------------------------ def _construct_UI(self): """ Construct User Interface """ amp_units = ["dB", "V", "W"] bfont = QFont() bfont.setBold(True) lblTitle = QLabel(str(self.title), self) # field for widget title lblTitle.setFont(bfont) lblTitle.setWordWrap(True) lblUnits = QLabel("in", self) self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(amp_units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip( "<span>Unit for amplitude specifications:" " dB is attenuation (> 0); levels in V and W have to be < 1.</span>" ) # fit size dynamically to largest element: self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) # find index for default unit from dictionary and set the unit amp_idx = self.cmbUnitsA.findData(fb.fil[0]['amp_specs_unit']) if amp_idx < 0: amp_idx = 0 self.cmbUnitsA.setCurrentIndex(amp_idx) # initialize for dBs layHTitle = QHBoxLayout() # layout for title and unit layHTitle.addWidget(lblTitle) layHTitle.addWidget(lblUnits, Qt.AlignLeft) layHTitle.addWidget(self.cmbUnitsA, Qt.AlignLeft) layHTitle.addStretch(1) self.layGSpecs = QGridLayout() # sublayout for spec fields # set the title as the first (fixed) entry in grid layout. The other # fields are added and hidden dynamically in _show_entries and _hide_entries() self.layGSpecs.addLayout(layHTitle, 0, 0, 1, 2) self.layGSpecs.setAlignment(Qt.AlignLeft) # This is the top level widget, encompassing the other widgets self.frmMain = QFrame(self) self.frmMain.setLayout(self.layGSpecs) self.layVMain = QVBoxLayout() # Widget main layout self.layVMain.addWidget(self.frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) self.n_cur_labels = 0 # number of currently visible labels / qlineedits # - Build a list from all entries in the fil_dict dictionary starting # with "A" (= amplitude specifications of the current filter) # - Pass the list to update_UI which recreates the widget # ATTENTION: Entries need to be converted from QString to str for Py 2 new_labels = [str(l) for l in fb.fil[0] if l[0] == 'A'] self.update_UI(new_labels=new_labels) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs / EVENT MONITORING #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self._set_amp_unit) # ^ this also triggers the initial load_dict # DYNAMIC EVENT MONITORING # Every time a field is edited, call self._store_entry and # self.load_dict. This is achieved by dynamically installing and # removing event filters when creating / deleting subwidgets. # The event filter monitors the focus of the input fields. #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value in linear format with full precision (only if `spec_edited`== True) and display the stored value in selected format """ if isinstance(source, QLineEdit): # could be extended for other widgets if event.type() == QEvent.FocusIn: self.spec_edited = False self.load_dict() # store current entry in case new value can't be evaluated: fb.data_old = source.text() elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: # store entry self._store_entry(source) elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False self.load_dict() elif event.type() == QEvent.FocusOut: self._store_entry(source) # Call base class method to continue normal event processing: return super(AmplitudeSpecs, self).eventFilter(source, event) #------------------------------------------------------------- def update_UI(self, new_labels=()): """ Called from filter_specs.update_UI() and target_specs.update_UI(). Set labels and get corresponding values from filter dictionary. When number of entries has changed, the layout of subwidget is rebuilt, using - `self.qlabels`, a list with references to existing QLabel widgets, - `new_labels`, a list of strings from the filter_dict for the current filter design - 'num_new_labels`, their number - `self.n_cur_labels`, the number of currently visible labels / qlineedit fields """ state = new_labels[0] new_labels = new_labels[1:] # W_lbl = max([self.qfm.width(l) for l in new_labels]) # max. label width in pixel num_new_labels = len(new_labels) if num_new_labels < self.n_cur_labels: # less new labels/qlineedit fields than before self._hide_entries(num_new_labels) elif num_new_labels > self.n_cur_labels: # more new labels, create / show new ones self._show_entries(num_new_labels) tool_tipp_sb = "Min. attenuation resp. maximum level in (this) stop band" for i in range(num_new_labels): # Update ALL labels and corresponding values self.qlabels[i].setText(to_html(new_labels[i], frmt='bi')) self.qlineedit[i].setText(str(fb.fil[0][new_labels[i]])) self.qlineedit[i].setObjectName(new_labels[i]) # update ID if "sb" in new_labels[i].lower(): self.qlineedit[i].setToolTip("<span>" + tool_tipp_sb + " (> 0).</span>") elif "pb" in new_labels[i].lower(): self.qlineedit[i].setToolTip( "<span>Maximum ripple (> 0) in (this) pass band.<span/>" ) qstyle_widget(self.qlineedit[i], state) self.n_cur_labels = num_new_labels # update number of currently visible labels self.load_dict( ) # display rounded filter dict entries in selected unit #------------------------------------------------------------------------------ def load_dict(self): """ Reload and reformat the amplitude textfields from filter dict when a new filter design algorithm is selected or when the user has changed the unit (V / W / dB): - Reload amplitude entries from filter dictionary and convert to selected to reflect changed settings unit. - Update the lineedit fields, rounded to specified format. """ unit = fb.fil[0]['amp_specs_unit'] filt_type = fb.fil[0]['ft'] for i in range(len(self.qlineedit)): amp_label = str(self.qlineedit[i].objectName()) amp_value = lin2unit(fb.fil[0][amp_label], filt_type, amp_label, unit=unit) if not self.qlineedit[i].hasFocus(): # widget has no focus, round the display self.qlineedit[i].setText(params['FMT'].format(amp_value)) else: # widget has focus, show full precision self.qlineedit[i].setText(str(amp_value)) #------------------------------------------------------------------------------ def _set_amp_unit(self, source): """ Store unit for amplitude in filter dictionary, reload amplitude spec entries via load_dict and fire a sigUnitChanged signal """ fb.fil[0]['amp_specs_unit'] = qget_cmb_box(self.cmbUnitsA, data=False) self.load_dict() self.sig_tx.emit({'sender': __name__, 'view_changed': 'a_unit'}) #------------------------------------------------------------------------------ def _store_entry(self, source): """ When the textfield of `source` has been edited (flag `self.spec_edited` = True), transform the amplitude spec back to linear unit setting and store it in filter dict. This is triggered by `QEvent.focusOut` Spec entries are *always* stored in linear units; only the displayed values are adapted to the amplitude unit, not the dictionary! """ if self.spec_edited: unit = str(self.cmbUnitsA.currentText()) filt_type = fb.fil[0]['ft'] amp_label = str(source.objectName()) amp_value = safe_eval(source.text(), fb.data_old, sign='pos') fb.fil[0].update( {amp_label: unit2lin(amp_value, filt_type, amp_label, unit)}) self.sig_tx.emit({'sender': __name__, 'specs_changed': 'a_specs'}) self.spec_edited = False # reset flag self.load_dict() #------------------------------------------------------------- def _hide_entries(self, num_new_labels): """ Hide subwidgets so that only `num_new_labels` subwidgets are visible """ for i in range(num_new_labels, len(self.qlabels)): self.qlabels[i].hide() self.qlineedit[i].hide() #------------------------------------------------------------------------ def _show_entries(self, num_new_labels): """ - check whether enough subwidgets (QLabel und QLineEdit) exist for the the required number of `num_new_labels`: - create new ones if required - initialize them with dummy information - install eventFilter for new QLineEdit widgets so that the filter dict is updated automatically when a QLineEdit field has been edited. - if enough subwidgets exist already, make enough of them visible to show all spec fields """ num_tot_labels = len( self.qlabels) # number of existing labels (vis. + invis.) if num_tot_labels < num_new_labels: # new widgets need to be generated for i in range(num_tot_labels, num_new_labels): self.qlabels.append(QLabel(self)) self.qlabels[i].setText(to_html("dummy", frmt='bi')) self.qlineedit.append(QLineEdit("")) self.qlineedit[i].setObjectName("dummy") self.qlineedit[i].installEventFilter(self) # filter events # first entry is title self.layGSpecs.addWidget(self.qlabels[i], i + 1, 0) self.layGSpecs.addWidget(self.qlineedit[i], i + 1, 1) else: # make the right number of widgets visible for i in range(self.n_cur_labels, num_new_labels): self.qlabels[i].show() self.qlineedit[i].show()
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.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._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("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 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.chkSpecs.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.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) #------------------------------------------------------------------------------ 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.chkAlign.setVisible(self.chkPhase.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.cmbUnitsA.currentText() == 'Auto': self.unitA = fb.fil[0]['amp_specs_unit'] else: self.unitA = self.cmbUnitsA.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.chkZerophase.setCheckable(self.unitA == 'V') self.chkZerophase.setEnabled(self.unitA == 'V') self.specs = self.chkSpecs.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.chkZerophase.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.chkSpecs.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.chkPhase.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.chkAlign.isChecked(): # Align gridlines between H(f) and phi nicely self.align_y_axes(self.ax, self.ax_p) self.mplwidget.redraw()
class FIR_DF_wdg(QWidget): """ Widget for entering word formats & quantization, also instantiates fixpoint filter class :class:`FilterFIR`. """ # incoming, sig_rx = pyqtSignal(object) # outcgoing sig_tx = pyqtSignal(object) def __init__(self, parent): super(FIR_DF_wdg, self).__init__(parent) self.title = ("<b>Direct-Form (DF) FIR Filter</b><br />" "Standard FIR topology.") self.img_name = "fir_df.png" self._construct_UI() # Construct an instance of the fixpoint filter using the settings from # the 'fxqc' quantizer dict self.construct_fixp_filter() #------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the UI with widgets for coefficient format and input and output quantization """ if not 'QA' in fb.fil[0]['fxqc']: fb.fil[0]['fxqc']['QA'] = {} set_dict_defaults(fb.fil[0]['fxqc']['QA'], {'WI':0, 'WF':30, 'W':32, 'ovfl':'wrap', 'quant':'floor'}) self.wdg_w_coeffs = UI_W(self, fb.fil[0]['fxqc']['QCB'], id='w_coeff', label='Coeff. Format <i>B<sub>I.F </sub></i>:', tip_WI='Number of integer bits - edit in the "b,a" tab', tip_WF='Number of fractional bits - edit in the "b,a" tab', WI = fb.fil[0]['fxqc']['QCB']['WI'], WF = fb.fil[0]['fxqc']['QCB']['WF']) # self.wdg_q_coeffs = UI_Q(self, fb.fil[0]['fxqc']['QCB'], # cur_ov=fb.fil[0]['fxqc']['QCB']['ovfl'], # cur_q=fb.fil[0]['fxqc']['QCB']['quant']) # self.wdg_q_coeffs.sig_tx.connect(self.update_q_coeff) self.wdg_w_accu = UI_W(self, fb.fil[0]['fxqc']['QA'], label='', id='w_accu', fractional=True, combo_visible=True) self.wdg_q_accu = UI_Q(self, fb.fil[0]['fxqc']['QA'], id='q_accu', label='Accu Format <i>Q<sub>A </sub></i>:') # initial setting for accumulator cmbW = qget_cmb_box(self.wdg_w_accu.cmbW, data=False) self.wdg_w_accu.ledWF.setEnabled(cmbW=='man') self.wdg_w_accu.ledWI.setEnabled(cmbW=='man') #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs & EVENTFILTERS #---------------------------------------------------------------------- self.wdg_w_coeffs.sig_tx.connect(self.update_q_coeff) self.wdg_w_accu.sig_tx.connect(self.process_sig_rx) self.wdg_q_accu.sig_tx.connect(self.process_sig_rx) #------------------------------------------------------------------------------ layVWdg = QVBoxLayout() layVWdg.setContentsMargins(0,0,0,0) layVWdg.addWidget(self.wdg_w_coeffs) # layVWdg.addWidget(self.wdg_q_coeffs) layVWdg.addWidget(self.wdg_q_accu) layVWdg.addWidget(self.wdg_w_accu) layVWdg.addStretch() self.setLayout(layVWdg) #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): logger.debug("sig_rx:\n{0}".format(pprint_log(dict_sig))) # check whether anything needs to be done locally # could also check here for 'quant', 'ovfl', 'WI', 'WF' (not needed at the moment) # if not, just pass the dict if 'ui' in dict_sig: if dict_sig['id'] == 'w_coeff': # coefficient format updated """ Update coefficient quantization settings and coefficients. The new values are written to the fixpoint coefficient dict as `fb.fil[0]['fxqc']['QCB']` and `fb.fil[0]['fxqc']['b']`. """ fb.fil[0]['fxqc'].update(self.ui2dict()) elif dict_sig['ui'] == 'cmbW': cmbW = qget_cmb_box(self.wdg_w_accu.cmbW, data=False) self.wdg_w_accu.ledWF.setEnabled(cmbW=='man') self.wdg_w_accu.ledWI.setEnabled(cmbW=='man') if cmbW in {'full', 'auto'}: self.dict2ui() self.sig_tx.emit({'sender':__name__, 'specs_changed':'cmbW'}) else: return dict_sig.update({'sender':__name__}) # currently only local self.sig_tx.emit(dict_sig) #------------------------------------------------------------------------------ def update_q_coeff(self, dict_sig): """ Update coefficient quantization settings and coefficients. The new values are written to the fixpoint coefficient dict as `fb.fil[0]['fxqc']['QCB']` and `fb.fil[0]['fxqc']['b']`. """ logger.debug("update q_coeff - dict_sig:\n{0}".format(pprint_log(dict_sig))) #dict_sig.update({'ui':'C'+dict_sig['ui']}) fb.fil[0]['fxqc'].update(self.ui2dict()) logger.debug("b = {0}".format(pprint_log(fb.fil[0]['fxqc']['b']))) self.process_sig_rx(dict_sig) #------------------------------------------------------------------------------ def update_accu_settings(self): """ Calculate number of extra integer bits needed in the accumulator (bit growth) depending on the coefficient area (sum of absolute coefficient values) for `cmbW == 'auto'` or depending on the number of coefficients for `cmbW == 'full'`. The latter works for arbitrary coefficients but requires more bits. The new values are written to the fixpoint coefficient dict `fb.fil[0]['fxqc']['QA']`. """ try: if qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "full": A_coeff = int(np.ceil(np.log2(len(fb.fil[0]['fxqc']['b'])))) elif qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "auto": A_coeff = int(np.ceil(np.log2(np.sum(np.abs(fb.fil[0]['ba'][0]))))) except Exception as e: logger.error(e) return if qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "full" or\ qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "auto": fb.fil[0]['fxqc']['QA']['WF'] = fb.fil[0]['fxqc']['QI']['WF']\ + fb.fil[0]['fxqc']['QCB']['WF'] fb.fil[0]['fxqc']['QA']['WI'] = fb.fil[0]['fxqc']['QI']['WI']\ + fb.fil[0]['fxqc']['QCB']['WI'] + A_coeff # calculate total accumulator word length fb.fil[0]['fxqc']['QA']['W'] = fb.fil[0]['fxqc']['QA']['WI']\ + fb.fil[0]['fxqc']['QA']['WF'] + 1 # update quantization settings fb.fil[0]['fxqc']['QA'].update(self.wdg_q_accu.q_dict) self.wdg_w_accu.dict2ui(fb.fil[0]['fxqc']['QA']) #------------------------------------------------------------------------------ def dict2ui(self): """ Update all parts of the UI that need to be updated when specs have been changed outside this class, e.g. coefficients and coefficient wordlength. This also provides the initial setting for the widgets when the filter has been changed. This is called from one level above by :class:`pyfda.input_widgets.input_fixpoint_specs.Input_Fixpoint_Specs`. """ fxqc_dict = fb.fil[0]['fxqc'] if not 'QA' in fxqc_dict: fxqc_dict.update({'QA':{}}) # no accumulator settings in dict yet logger.warning("QA key missing") if not 'QCB' in fxqc_dict: fxqc_dict.update({'QCB':{}}) # no coefficient settings in dict yet logger.warning("QCB key missing") self.wdg_w_coeffs.dict2ui(fxqc_dict['QCB']) # update coefficient wordlength self.update_accu_settings() # update accumulator settings #------------------------------------------------------------------------------ def ui2dict(self): """ Read out the quantization subwidgets and store their settings in the central fixpoint dictionary `fb.fil[0]['fxqc']` using the keys described below. Coefficients are quantized with these settings in the subdictionary under the key 'b'. Additionally, these subdictionaries are returned to the caller (``input_fixpoint_specs``) where they are used to update ``fb.fil[0]['fxqc']`` Parameters ---------- None Returns ------- fxqc_dict : dict containing the following keys and values: - 'QCB': dictionary with coefficients quantization settings - 'QA': dictionary with accumulator quantization settings - 'b' : list of coefficients in integer format """ fxqc_dict = fb.fil[0]['fxqc'] if not 'QA' in fxqc_dict: fxqc_dict.update({'QA':self.wdg_w_accu.q_dict}) # no accumulator settings in dict yet logger.warning("Empty dict 'fxqc['QA]'!") else: fxqc_dict['QA'].update(self.wdg_w_accu.q_dict) if not 'QCB' in fxqc_dict: fxqc_dict.update({'QCB':self.wdg_w_coeffs.q_dict}) # no coefficient settings in dict yet logger.warning("Empty dict 'fxqc['QCB]'!") else: fxqc_dict['QCB'].update(self.wdg_w_coeffs.q_dict) fxqc_dict.update({'b':self.wdg_w_coeffs.quant_coeffs(self.wdg_w_coeffs.q_dict, fb.fil[0]['ba'][0])}) return fxqc_dict #------------------------------------------------------------------------------ def construct_fixp_filter(self): """ Construct an instance of the fixpoint filter object using the settings from the 'fxqc' quantizer dict """ p = fb.fil[0]['fxqc'] if not all(np.isfinite(p['b'])): logger.error("Coefficients contain non-finite values!") return if any(np.iscomplex(p['b'])): logger.error("Coefficients contain complex values!") return self.fixp_filter = FIR() #------------------------------------------------------------------------------ def to_verilog(self): """ Convert the migen description to Verilog """ return verilog.convert(self.fixp_filter, ios={self.fixp_filter.i, self.fixp_filter.o}) #------------------------------------------------------------------------------ def tb_wdg_stim(self, stimulus, outputs): """ use stimulus list from widget as input to filter """ for x in stimulus: yield self.fixp_filter.i.eq(int(x)) # pass one stimulus value to filter outputs.append((yield self.fixp_filter.o)) # append filter output to output list yield # next x until stimulus is used up #------------------------------------------------------------------------------ def run_sim(self, stimulus): """ Pass stimuli and run filter simulation, see https://reconfig.io/2018/05/hello_world_migen https://github.com/m-labs/migen/blob/master/examples/sim/fir.py """ response = [] testbench = self.tb_wdg_stim(stimulus, response) run_simulation(self.fixp_filter, testbench) return response
class Input_Coeffs(QWidget): """ Create widget with a (sort of) model-view architecture for viewing / editing / entering data contained in `self.ba` which is a list of two numpy arrays: - `self.ba[0]` contains the numerator coefficients ("b") - `self.ba[1]` contains the denominator coefficients ("a") The list don't neccessarily have the same length but they are always defined. For FIR filters, `self.ba[1][0] = 1`, all other elements are zero. The length of both lists can be egalized with `self._equalize_ba_length()`. Views / formats are handled by the ItemDelegate() class. """ sig_tx = pyqtSignal(object) # emitted when filter has been saved sig_rx = pyqtSignal(object) # incoming from input_tab_widgets def __init__(self, parent): super(Input_Coeffs, self).__init__(parent) self.opt_widget = None # handle for pop-up options widget self.tool_tip = "Display and edit filter coefficients." self.tab_label = "b,a" self.data_changed = True # initialize flag: filter data has been changed self.fx_specs_changed = True # fixpoint specs have been changed outside self.ui = Input_Coeffs_UI(self) # create the UI part with buttons etc. self._construct_UI() #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from sig_rx """ logger.debug("process_sig_rx(): vis={0}\n{1}"\ .format(self.isVisible(), pprint_log(dict_sig))) if dict_sig['sender'] == __name__: logger.debug("Stopped infinite loop\n{0}".format(pprint_log(dict_sig))) return if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'csv': self.ui._set_load_save_icons() elif self.isVisible(): if self.data_changed or 'data_changed' in dict_sig: self.load_dict() self.data_changed = False if self.fx_specs_changed or ('fx_sim' in dict_sig and dict_sig['fx_sim'] == 'specs_changed'): self.qdict2ui() self.fx_specs_changed = False else: # TODO: draw wouldn't be necessary for 'view_changed', only update view if 'data_changed' in dict_sig: self.data_changed = True elif 'fx_sim' in dict_sig and dict_sig['fx_sim'] == 'specs_changed': self.fx_specs_changed = True #------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - top chkbox row - coefficient table - two bottom rows with action buttons """ # --------------------------------------------------------------------- # Coefficient table widget # --------------------------------------------------------------------- self.tblCoeff = QTableWidget(self) self.tblCoeff.setAlternatingRowColors(True) self.tblCoeff.horizontalHeader().setHighlightSections(True) # highlight when selected self.tblCoeff.horizontalHeader().setFont(self.ui.bfont) # self.tblCoeff.QItemSelectionModel.Clear self.tblCoeff.setDragEnabled(True) # self.tblCoeff.setDragDropMode(QAbstractItemView.InternalMove) # doesn't work like intended self.tblCoeff.setItemDelegate(ItemDelegate(self)) # ============== Main UI Layout ===================================== layVMain = QVBoxLayout() layVMain.setAlignment(Qt.AlignTop) # this affects only the first widget (intended here) layVMain.addWidget(self.ui) layVMain.addWidget(self.tblCoeff) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) self.myQ = fx.Fixed(fb.fil[0]['fxqc']['QCB']) # initialize fixpoint object self.load_dict() # initialize + refresh table with default values from filter dict # TODO: this needs to be optimized - self._refresh is being called in both routines self._set_number_format() #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL (UI) SIGNALS & SLOTs #---------------------------------------------------------------------- # wdg.textChanged() is emitted when contents of widget changes # wdg.textEdited() is only emitted for user changes # wdg.editingFinished() is only emitted for user changes self.ui.butEnable.clicked.connect(self._refresh_table) self.ui.spnDigits.editingFinished.connect(self._refresh_table) self.ui.cmbQFrmt.currentIndexChanged.connect(self._set_number_format) self.ui.butFromTable.clicked.connect(self._copy_from_table) self.ui.butToTable.clicked.connect(self._copy_to_table) self.ui.cmbFilterType.currentIndexChanged.connect(self._filter_type) self.ui.butDelCells.clicked.connect(self._delete_cells) self.ui.butAddCells.clicked.connect(self._add_cells) self.ui.butLoad.clicked.connect(self.load_dict) self.ui.butSave.clicked.connect(self._save_dict) self.ui.butClear.clicked.connect(self._clear_table) self.ui.ledEps.editingFinished.connect(self._set_eps) self.ui.butSetZero.clicked.connect(self._set_coeffs_zero) # store new settings and refresh table self.ui.cmbFormat.currentIndexChanged.connect(self.ui2qdict) self.ui.cmbQOvfl.currentIndexChanged.connect(self.ui2qdict) self.ui.cmbQuant.currentIndexChanged.connect(self.ui2qdict) self.ui.ledWF.editingFinished.connect(self.ui2qdict) self.ui.ledWI.editingFinished.connect(self.ui2qdict) self.ui.ledW.editingFinished.connect(self._W_changed) self.ui.ledScale.editingFinished.connect(self._set_scale) self.ui.butQuant.clicked.connect(self.quant_coeffs) self.ui.sig_tx.connect(self.sig_tx) # ===================================================================== #------------------------------------------------------------------------------ def _filter_type(self, ftype=None): """ Get / set 'FIR' and 'IIR' filter from cmbFilterType combobox and set filter dict and table properties accordingly. When argument fil_type is not None, set the combobox accordingly. Reload from filter dict unless ftype is specified [does this make sense?!] """ if ftype in {'FIR', 'IIR'}: ret=qset_cmb_box(self.ui.cmbFilterType, ftype) if ret == -1: logger.warning("Unknown filter type {0}".format(ftype)) if self.ui.cmbFilterType.currentText() == 'IIR': fb.fil[0]['ft'] = 'IIR' self.col = 2 self.tblCoeff.setColumnCount(2) self.tblCoeff.setHorizontalHeaderLabels(["b", "a"]) else: fb.fil[0]['ft'] = 'FIR' self.col = 1 self.tblCoeff.setColumnCount(1) self.tblCoeff.setHorizontalHeaderLabels(["b"]) self.ba[1] = np.zeros_like(self.ba[1]) # enforce FIR filter self.ba[1][0] = 1. self._equalize_ba_length() qstyle_widget(self.ui.butSave, 'changed') self._refresh_table() #------------------------------------------------------------------------------ def _W_changed(self): """ Set fractional and integer length `WF` and `WI` when wordlength `W` has been changed. Try to preserve `WI` or `WF` settings depending on the number format (integer or fractional). """ W = safe_eval(self.ui.ledW.text(), self.myQ.W, return_type='int', sign='pos') if W < 2: logger.warn("W must be > 1, restoring previous value.") W = self.myQ.W # fall back to previous value self.ui.ledW.setText(str(W)) if qget_cmb_box(self.ui.cmbQFrmt) == 'qint': # integer format, preserve WI bits WI = W - self.myQ.WF - 1 self.ui.ledWI.setText(str(WI)) self.ui.ledScale.setText(str(1 << (W-1))) else: # fractional format, preserve WF bit setting WF = W - self.myQ.WI - 1 if WF < 0: self.ui.ledWI.setText(str(W - 1)) WF = 0 self.ui.ledWF.setText(str(WF)) self.ui2qdict() #------------------------------------------------------------------------------ def _set_scale(self): """ Triggered by `ui.ledScale` Set scale for calculating floating point value from fixpoint representation and vice versa """ # if self.ui.ledScale.isModified() ... self.ui.ledScale.setModified(False) scale = safe_eval(self.ui.ledScale.text(), self.myQ.scale, return_type='float', sign='pos') self.ui.ledScale.setText(str(scale)) self.ui2qdict() #------------------------------------------------------------------------------ def _refresh_table_item(self, row, col): """ Refresh the table item with the index `row, col` from self.ba """ item = self.tblCoeff.item(row, col) if item: # does item exist? item.setText(str(self.ba[col][row]).strip('()')) else: # no, construct it: self.tblCoeff.setItem(row,col,QTableWidgetItem( str(self.ba[col][row]).strip('()'))) self.tblCoeff.item(row, col).setTextAlignment(Qt.AlignRight|Qt.AlignCenter) #------------------------------------------------------------------------------ def _refresh_table(self): """ (Re-)Create the displayed table from `self.ba` (list with 2 one-dimensional numpy arrays). Data is displayed via `ItemDelegate.displayText()` in the number format set by `self.frmt`. - self.ba[0] -> b coefficients - self.ba[1] -> a coefficients The table dimensions are set according to the filter type set in `fb.fil[0]['ft']` which is either 'FIR' or 'IIR' and by the number of rows in `self.ba`. Called at the end of nearly every method. """ if np.ndim(self.ba) == 1 or fb.fil[0]['ft'] == 'FIR': self.num_rows = len(self.ba[0]) else: self.num_rows = max(len(self.ba[1]), len(self.ba[0])) # logger.warning("np.shape(ba) = {0}".format(np.shape(self.ba))) params['FMT_ba'] = int(self.ui.spnDigits.text()) # When format is 'float', disable all fixpoint options is_float = (qget_cmb_box(self.ui.cmbFormat, data=False).lower() == 'float') self.ui.spnDigits.setVisible(is_float) # number of digits can only be selected self.ui.lblDigits.setVisible(is_float) # for format = 'float' self.ui.cmbQFrmt.setVisible(not is_float) # hide unneeded widgets for format = 'float' self.ui.lbl_W.setVisible(not is_float) self.ui.ledW.setVisible(not is_float) self.ui.frmQSettings.setVisible(not is_float) # hide all q-settings for float if self.ui.butEnable.isChecked(): self.ui.butEnable.setIcon(QIcon(':/circle-x.svg')) self.ui.frmButtonsCoeffs.setVisible(True) self.tblCoeff.setVisible(True) # check whether filter is FIR and only needs one column if fb.fil[0]['ft'] == 'FIR': self.num_cols = 1 self.tblCoeff.setColumnCount(1) self.tblCoeff.setHorizontalHeaderLabels(["b"]) qset_cmb_box(self.ui.cmbFilterType, 'FIR') else: self.num_cols = 2 self.tblCoeff.setColumnCount(2) self.tblCoeff.setHorizontalHeaderLabels(["b", "a"]) qset_cmb_box(self.ui.cmbFilterType, 'IIR') self.ba[1][0] = 1.0 # restore fa[0] = 1 of denonimator polynome self.tblCoeff.setRowCount(self.num_rows) self.tblCoeff.setColumnCount(self.num_cols) # Create strings for index column (vertical header), starting with "0" idx_str = [str(n) for n in range(self.num_rows)] self.tblCoeff.setVerticalHeaderLabels(idx_str) self.tblCoeff.blockSignals(True) for col in range(self.num_cols): for row in range(self.num_rows): self._refresh_table_item(row, col) # make a[0] selectable but not editable if fb.fil[0]['ft'] == 'IIR': item = self.tblCoeff.item(0,1) item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled) item.setFont(self.ui.bfont) self.tblCoeff.blockSignals(False) self.tblCoeff.resizeColumnsToContents() self.tblCoeff.resizeRowsToContents() self.tblCoeff.clearSelection() else: self.ui.frmButtonsCoeffs.setVisible(False) self.ui.butEnable.setIcon(QIcon(':/circle-check.svg')) self.tblCoeff.setVisible(False) #------------------------------------------------------------------------------ def load_dict(self): """ Load all entries from filter dict `fb.fil[0]['ba']` into the coefficient list `self.ba` and update the display via `self._refresh_table()`. The filter dict is a "normal" 2D-numpy float array for the b and a coefficients while the coefficient register `self.ba` is a list of two float ndarrays to allow for different lengths of b and a subarrays while adding / deleting items. """ self.ba = [0., 0.] # initial list with two elements self.ba[0] = np.array(fb.fil[0]['ba'][0]) # deep copy from filter dict to self.ba[1] = np.array(fb.fil[0]['ba'][1]) # coefficient register # set quantization comboBoxes from dictionary self.qdict2ui() self._refresh_table() qstyle_widget(self.ui.butSave, 'normal') #------------------------------------------------------------------------------ def _copy_options(self): """ Set options for copying to/from clipboard or file. """ self.opt_widget = CSV_option_box(self) # important: Handle must be class attribute #self.opt_widget.show() # modeless dialog, i.e. non-blocking self.opt_widget.exec_() # modal dialog (blocking) #------------------------------------------------------------------------------ def _copy_from_table(self): """ Copy data from coefficient table `self.tblCoeff` to clipboard / file in CSV format. """ qtable2text(self.tblCoeff, self.ba, self, 'ba', self.myQ.frmt, title="Export Filter Coefficients") #------------------------------------------------------------------------------ def _copy_to_table(self): """ Read data from clipboard / file and copy it to `self.ba` as float / cmplx # TODO: More checks for swapped row <-> col, single values, wrong data type ... """ data_str = qtext2table(self, 'ba', title="Import Filter Coefficients") # returns ndarray of str if data_str is None: # file operation has been aborted or some other error return logger.debug("importing data: dim - shape = {0} - {1} - {2}\n{3}"\ .format(type(data_str), np.ndim(data_str), np.shape(data_str), data_str)) conv = self.myQ.frmt2float # frmt2float_vec? frmt = self.myQ.frmt if np.ndim(data_str) > 1: num_cols, num_rows = np.shape(data_str) orientation_horiz = num_cols > num_rows # need to transpose data elif np.ndim(data_str) == 1: num_rows = len(data_str) num_cols = 1 orientation_horiz = False else: logger.error("Imported data is a single value or None.") return None logger.info("_copy_to_table: c x r = {0} x {1}".format(num_cols, num_rows)) if orientation_horiz: self.ba = [[],[]] for c in range(num_cols): self.ba[0].append(conv(data_str[c][0], frmt)) if num_rows > 1: self.ba[1].append(conv(data_str[c][1], frmt)) if num_rows > 1: self._filter_type(ftype='IIR') else: self._filter_type(ftype='FIR') else: self.ba[0] = [conv(s, frmt) for s in data_str[0]] if num_cols > 1: self.ba[1] = [conv(s, frmt) for s in data_str[1]] self._filter_type(ftype='IIR') else: self.ba[1] = [1] self._filter_type(ftype='FIR') self.ba[0] = np.asarray(self.ba[0]) self.ba[1] = np.asarray(self.ba[1]) self._equalize_ba_length() qstyle_widget(self.ui.butSave, 'changed') self._refresh_table() #------------------------------------------------------------------------------ def _set_number_format(self): """ Triggered by `contruct_UI()`, `qdict2ui()`and by `ui.cmbQFrmt.currentIndexChanged()` Set one of three number formats: Integer, fractional, normalized fractional (triggered by self.ui.cmbQFrmt combobox) """ qfrmt = qget_cmb_box(self.ui.cmbQFrmt) is_qfrac = False W = safe_eval(self.ui.ledW.text(), self.myQ.W, return_type='int', sign='pos') if qfrmt == 'qint': self.ui.ledWI.setText(str(W - 1)) self.ui.ledWF.setText("0") elif qfrmt == 'qnfrac': # normalized fractional format self.ui.ledWI.setText("0") self.ui.ledWF.setText(str(W - 1)) else: # qfrmt == 'qfrac': is_qfrac = True WI = safe_eval(self.ui.ledWI.text(), self.myQ.WI, return_type='int') self.ui.ledScale.setText(str(1 << WI)) self.ui.ledWI.setEnabled(is_qfrac) self.ui.lblDot.setEnabled(is_qfrac) self.ui.ledWF.setEnabled(is_qfrac) self.ui.ledW.setEnabled(not is_qfrac) self.ui.ledScale.setEnabled(False) self.ui2qdict() # save UI to dict and to class attributes #------------------------------------------------------------------------------ def _update_MSB_LSB(self): """ Update the infos (LSB, MSB, Max) """ self.ui.lblLSB.setText("{0:.{1}g}".format(self.myQ.LSB, params['FMT_ba'])) self.ui.lblMSB.setText("{0:.{1}g}".format(self.myQ.MSB, params['FMT_ba'])) self.ui.lblMAX.setText("{0:.6g}".format(self.myQ.MAX)) #------------------------------------------------------------------------------ def qdict2ui(self): """ Triggered by: - process_sig_rx() if self.fx_specs_changed or dict_sig['fx_sim'] == 'specs_changed' - Set the UI from the quantization dict and update the fixpoint object. When neither WI == 0 nor WF == 0, set the quantization format to general fractional format qfrac. """ self.ui.ledWI.setText(qstr(fb.fil[0]['fxqc']['QCB']['WI'])) self.ui.ledWF.setText(qstr(fb.fil[0]['fxqc']['QCB']['WF'])) self.ui.ledW.setText(qstr(fb.fil[0]['fxqc']['QCB']['W'])) if fb.fil[0]['fxqc']['QCB']['WI'] != 0 and fb.fil[0]['fxqc']['QCB']['WF'] != 0: qset_cmb_box(self.ui.cmbQFrmt, 'qfrac', data=True) self.ui.ledScale.setText(qstr(fb.fil[0]['fxqc']['QCB']['scale'])) qset_cmb_box(self.ui.cmbQuant, fb.fil[0]['fxqc']['QCB']['quant']) qset_cmb_box(self.ui.cmbQOvfl, fb.fil[0]['fxqc']['QCB']['ovfl']) self.myQ.setQobj(fb.fil[0]['fxqc']['QCB']) # update class attributes self._set_number_format() # quant format has been changed, update display self._update_MSB_LSB() #------------------------------------------------------------------------------ def ui2qdict(self): """ Triggered by modifying `ui.cmbFormat`, `ui.cmbQOvfl`, `ui.cmbQuant`, `ui.ledWF`, `ui.ledWI` or `ui.ledW` (via `_W_changed()`) or `ui.cmbQFrmt` (via `_set_number_format()`) or `ui.ledScale()` (via `_set_scale()`) or 'qdict2ui()' via `_set_number_format()` Read out the settings of the quantization comboboxes. - Store them in the filter dict `fb.fil[0]['fxqc']['QCB']` and as class attributes in the fixpoint object `self.myQ` - Emit a signal with `'view_changed':'q_coeff'` - Refresh the table """ fb.fil[0]['fxqc']['QCB'] = { 'WI':safe_eval(self.ui.ledWI.text(), self.myQ.WI, return_type='int'), 'WF':safe_eval(self.ui.ledWF.text(), self.myQ.WF, return_type='int', sign='poszero'), 'W':safe_eval(self.ui.ledW.text(), self.myQ.W, return_type='int', sign='pos'), 'quant':qstr(self.ui.cmbQuant.currentText()), 'ovfl':qstr(self.ui.cmbQOvfl.currentText()), 'frmt':qstr(self.ui.cmbFormat.currentText().lower()), 'scale':qstr(self.ui.ledScale.text()) } self.myQ.setQobj(fb.fil[0]['fxqc']['QCB']) # update fixpoint object self.sig_tx.emit({'sender':__name__, 'view_changed':'q_coeff'}) self._update_MSB_LSB() self._refresh_table() #------------------------------------------------------------------------------ def _save_dict(self): """ Save the coefficient register `self.ba` to the filter dict `fb.fil[0]['ba']`. """ logger.debug("_save_dict called") fb.fil[0]['N'] = max(len(self.ba[0]), len(self.ba[1])) - 1 self.ui2qdict() if fb.fil[0]['ft'] == 'IIR': fb.fil[0]['fc'] = 'Manual_IIR' else: fb.fil[0]['fc'] = 'Manual_FIR' # save, check and convert coeffs, check filter type try: fil_save(fb.fil[0], self.ba, 'ba', __name__) except Exception as e: # catch exception due to malformatted coefficients: logger.error("While saving the filter coefficients, " "the following error occurred:\n{0}".format(e)) if __name__ == '__main__': self.load_dict() # only needed for stand-alone test self.sig_tx.emit({'sender':__name__, 'data_changed':'input_coeffs'}) # -> input_tab_widgets qstyle_widget(self.ui.butSave, 'normal') #------------------------------------------------------------------------------ def _clear_table(self): """ Clear self.ba: Initialize coeff for a poles and a zero @ origin, a = b = [1; 0]. Refresh QTableWidget """ self.ba = [np.asarray([1., 0.]), np.asarray([1., 0.])] self._refresh_table() qstyle_widget(self.ui.butSave, 'changed') #------------------------------------------------------------------------------ def _equalize_ba_length(self): """ test and equalize if b and a subarray have different lengths: """ try: a_len = len(self.ba[1]) except IndexError: self.ba.append(np.array(1)) a_len = 1 D = len(self.ba[0]) - a_len if D > 0: # b is longer than a self.ba[1] = np.append(self.ba[1], np.zeros(D)) elif D < 0: # a is longer than b if fb.fil[0]['ft'] == 'IIR': self.ba[0] = np.append(self.ba[0], np.zeros(-D)) else: self.ba[1] = self.ba[1][:D] # discard last D elements of a #------------------------------------------------------------------------------ def _delete_cells(self): """ Delete all selected elements in self.ba by: - determining the indices of all selected cells in the P and Z arrays - deleting elements with those indices - equalizing the lengths of b and a array by appending the required number of zeros. When nothing is selected, delete the last row. Finally, the QTableWidget is refreshed from self.ba. """ sel = qget_selected(self.tblCoeff)['sel'] # get indices of all selected cells if not any(sel) and len(self.ba[0]) > 0: # delete last row self.ba = np.delete(self.ba, -1, axis=1) elif np.all(sel[0] == sel[1]) or fb.fil[0]['ft'] == 'FIR': # only complete rows selected or FIR -> delete row self.ba = np.delete(self.ba, sel[0], axis=1) else: self.ba[0][sel[0]] = 0 self.ba[1][sel[1]] = 0 #self.ba[0] = np.delete(self.ba[0], sel[0]) #self.ba[1] = np.delete(self.ba[1], sel[1]) # test and equalize if b and a array have different lengths: self._equalize_ba_length() # if length is less than 2, clear the table: this ain't no filter! if len(self.ba[0]) < 2: self._clear_table() # sets 'changed' attribute else: self._refresh_table() qstyle_widget(self.ui.butSave, 'changed') #------------------------------------------------------------------------------ def _add_cells(self): """ Add the number of selected rows to self.ba and fill new cells with zeros from the bottom. If nothing is selected, add one row at the bottom. Refresh QTableWidget. """ # get indices of all selected cells sel = qget_selected(self.tblCoeff)['sel'] if not any(sel): # nothing selected, append one row of zeros to table self.ba = np.insert(self.ba, len(self.ba[0]), 0, axis=1) #"insert" row after last elif np.all(sel[0] == sel[1]) or fb.fil[0]['ft'] == 'FIR': # only complete rows selected self.ba = np.insert(self.ba, sel[0], 0, axis=1) # elif len(sel[0]) == len(sel[1]): # self.ba = np.insert(self.ba, sel, 0, axis=1) # not allowed, sel needs to be a scalar or one-dimensional else: logger.warning("It is only possible to insert complete rows!") # The following doesn't work because the subarrays wouldn't have # the same length for a moment #self.ba[0] = np.insert(self.ba[0], sel[0], 0) #self.ba[1] = np.insert(self.ba[1], sel[1], 0) return # insert 'sel' contiguous rows before 'row': # self.ba[0] = np.insert(self.ba[0], row, np.zeros(sel)) self._equalize_ba_length() self._refresh_table() # don't tag as 'changed' when only zeros have been added at the end if any(sel): qstyle_widget(self.ui.butSave, 'changed') #------------------------------------------------------------------------------ def _set_eps(self): """ Set all coefficients = 0 in self.ba with a magnitude less than eps and refresh QTableWidget """ self.ui.eps = safe_eval(self.ui.ledEps.text(), return_type='float', sign='pos', alt_expr=self.ui.eps) self.ui.ledEps.setText(str(self.ui.eps)) #------------------------------------------------------------------------------ def _set_coeffs_zero(self): """ Set all coefficients = 0 in self.ba with a magnitude less than eps and refresh QTableWidget """ self._set_eps() idx = qget_selected(self.tblCoeff)['idx'] # get all selected indices test_val = 0. # value against which array is tested targ_val = 0. # value which is set when condition is true changed = False if not idx: # nothing selected, check whole table b_close = np.logical_and(np.isclose(self.ba[0], test_val, rtol=0, atol=self.ui.eps), (self.ba[0] != targ_val)) if np.any(b_close): # found at least one coeff where condition was true self.ba[0] = np.where(b_close, targ_val, self.ba[0]) changed = True if fb.fil[0]['ft'] == 'IIR': a_close = np.logical_and(np.isclose(self.ba[1], test_val, rtol=0, atol=self.ui.eps), (self.ba[1] != targ_val)) if np.any(a_close): self.ba[1] = np.where(a_close, targ_val, self.ba[1]) changed = True else: # only check selected cells for i in idx: if np.logical_and(np.isclose(self.ba[i[0]][i[1]], test_val, rtol=0, atol=self.ui.eps), (self.ba[i[0]][i[1]] != targ_val)): self.ba[i[0]][i[1]] = targ_val changed = True if changed: qstyle_widget(self.ui.butSave, 'changed') # mark save button as changed self._refresh_table() #------------------------------------------------------------------------------ def quant_coeffs(self): """ Quantize selected / all coefficients in self.ba and refresh QTableWidget """ idx = qget_selected(self.tblCoeff)['idx'] # get all selected indices if not idx: # nothing selected, quantize all elements self.ba[0] = self.myQ.fixp(self.ba, scaling='multdiv')[0] if fb.fil[0]['ft'] == "IIR": self.ba[1] = self.myQ.fixp(self.ba, scaling='multdiv')[0] else: for i in idx: self.ba[i[0]][i[1]] = self.myQ.fixp(self.ba[i[0]][i[1]], scaling = 'multdiv') qstyle_widget(self.ui.butSave, 'changed') self._refresh_table()
class pyFDA(QMainWindow): """ Create the main window consisting of a tabbed widget for entering filter specifications, poles / zeros etc. and another tabbed widget for plotting various filter characteristics QMainWindow is used here as it is a class that understands GUI elements like toolbar, statusbar, central widget, docking areas etc. """ sig_rx = pyqtSignal(object) # incoming # sig_tx = pyqtSignal(object) # outgoing def __init__(self, parent=None): super(QMainWindow, self).__init__() self.setAttribute(QtCore.Qt.WA_DeleteOnClose) # create clipboard instance that can be accessed from other modules fb.clipboard = QApplication.clipboard() # initialize the FilterTreeBuilder class: # read config file and construct filter tree from it _ = Tree_Builder() # TODO_ couldn't this be a function? self._construct_UI() def _construct_UI(self): """ Construct the main GUI, consisting of: - Tabbed input widgets (left side) - Tabbed plot widgets (right side) - Logger window (right side, below plot tab) """ # ============== UI Layout with H and V-Splitter ===================== inputTabWidgets = input_tab_widgets.InputTabWidgets( self) # input widgets pltTabWidgets = plot_tab_widgets.PlotTabWidgets(self) # plot widgets self.loggerWin = QPlainTextEdit(self) # logger window self.loggerWin.setReadOnly(True) # set custom right-button context menu policy self.loggerWin.setContextMenuPolicy(Qt.CustomContextMenu) self.loggerWin.customContextMenuRequested.connect( self.logger_win_context_menu) # create context menu and define actions and shortcuts self.popMenu = QMenu(self) self.popMenu.addAction('Select &All', self.loggerWin.selectAll, "Ctrl+A") self.popMenu.addAction('&Copy Selected', self.loggerWin.copy) self.popMenu.addSeparator() self.popMenu.addAction('Clear &Window', self.loggerWin.clear) # ============================================================================= # add logger window underneath plot Tab Widgets spltVPltLogger = QSplitter(QtCore.Qt.Vertical) spltVPltLogger.addWidget(pltTabWidgets) spltVPltLogger.addWidget(self.loggerWin) # create horizontal splitter that contains all subwidget groups spltHMain = QSplitter(QtCore.Qt.Horizontal) spltHMain.addWidget(inputTabWidgets) spltHMain.addWidget(spltVPltLogger) spltHMain.setStretchFactor(1, 4) # relative initial sizes of subwidgets spltHMain.setContentsMargins(*rc.params['wdg_margins']) spltHMain.setFocus() # make spltHMain occupy the main area of QMainWindow and make QMainWindow its parent !!! self.setCentralWidget(spltHMain) spltVPltLoggerH = spltVPltLogger.size().height() spltVPltLogger.setSizes( [int(spltVPltLoggerH * 0.95), int(spltVPltLoggerH * 0.05 - 8)]) self.setWindowTitle('pyFDA - Python Filter Design and Analysis') #=============== Menubar ======================================= # aboutAction = QAction('&About', self) # aboutAction.setShortcut('Ctrl+A') # aboutAction.setStatusTip('Info about pyFDA') # # menubar = self.menuBar() # fileMenu = menubar.addMenu('&About') # fileMenu.addAction(aboutAction) # self.statusMessage("Application is initialized.") #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- # Here, signals about spec and design changes from lower hierarchies # are distributed. At the moment, only changes in the input widgets are # routed to the plot widgets: inputTabWidgets.sig_tx.connect(pltTabWidgets.sig_rx) inputTabWidgets.sig_tx.connect(self.process_sig_rx) pltTabWidgets.sig_tx.connect(inputTabWidgets.sig_rx) # open pop-up "about" window #aboutAction.triggered.connect(self.aboutWindow) # trigger the close event in response to sigQuit generated in another subwidget: # inputTabWidgets.input_filter_specs.sigQuit.connect(self.close) # when a message has been written, pass it via signal-slot mechanism and # print it to logger window XStream.stdout().messageWritten.connect(self.loggerWin.appendHtml) #------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from sig_rx: - trigger close event in response to 'quit_program' emitted in another subwidget: """ logger.debug("Processing {0}: {1}".format( type(dict_sig).__name__, dict_sig)) if 'quit_program' in dict_sig: self.close() #============================================================================== # def statusMessage(self, message): # """ # Display a message in the statusbar. # """ # self.statusBar().showMessage(message) # # #============================================================================== def logger_win_context_menu(self, point): """ Show right mouse button context menu """ self.popMenu.exec_(self.loggerWin.mapToGlobal(point)) # ============================================================================= def closeEvent(self, event): """ reimplement QMainWindow.closeEvent() to prompt the user """ reply = QMessageBox.question(self, 'Message', "Quit pyFDA?", QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: # Clear clipboard before exit to avoid error message on older Qt versions # "QClipboard: Unable to receive an event from the clipboard manager # in a reasonable time fb.clipboard.clear() event.accept() else: event.ignore()
class Delay(QWidget): FRMT = 'zpk' # output format of delay filter widget info =""" **Delay widget** allows entering the number of **delays** :math:`N` :math:`T_S`. It is treated as a FIR filter, the number of delays is directly translated to a number of poles (:math:`N > 0`) or zeros (:math:`N < 0`). Obviously, there is no minimum design algorithm or no design algorithm at all :-) """ sig_tx = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.N = 5 self.ft = 'FIR' self.rt_dicts = ('com',) self.rt_dict = { 'COM': {'man': {'fo':('a', 'N'), 'msg':('a', "<span>Enter desired filter order <b><i>N</i></b>, corner " "frequencies of pass and stop band(s), <b><i>F<sub>PB</sub></i></b>" " and <b><i>F<sub>SB</sub></i></b> , and relative weight " "values <b><i>W </i></b> (1 ... 10<sup>6</sup>) to specify how well " "the bands are approximated.</span>") }, }, 'LP': {'man':{'wspecs': ('u','W_PB','W_SB'), 'tspecs': ('u', {'frq':('a','F_PB','F_SB'), 'amp':('u','A_PB','A_SB')}) }, }, 'HP': {'man':{'wspecs': ('u','W_SB','W_PB')}, }, 'BP': { }, 'BS': {'man':{'wspecs': ('u','W_PB','W_SB','W_PB2'), 'tspecs': ('u', {'frq':('a','F_PB','F_SB','F_SB2','F_PB2'), 'amp':('u','A_PB','A_SB','A_PB2')}) } } } self.info_doc = [] #-------------------------------------------------------------------------- def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter instance, fb.fil_inst. """ self.lbl_delay = QLabel("Delays", self) self.lbl_delay.setObjectName('wdg_lbl_delays') self.led_delay = QLineEdit(self) self.led_delay.setText(str(self.N)) self.led_delay.setObjectName('wdg_led_delay') self.led_delay.setToolTip("Number of delays, N > 0 produces poles, N < 0 zeros.") self.layHWin = QHBoxLayout() self.layHWin.setObjectName('wdg_layGWin') self.layHWin.addWidget(self.lbl_delay) self.layHWin.addWidget(self.led_delay) self.layHWin.setContentsMargins(0,0,0,0) # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits) self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layHWin) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.led_delay.editingFinished.connect(self._update_UI) # fires when edited line looses focus or when RETURN is pressed #---------------------------------------------------------------------- self._load_dict() # get initial / last setting from dictionary self._update_UI() def _update_UI(self): """ Update UI when line edit field is changed (here, only the text is read and converted to integer) and store parameter settings in filter dictionary """ self.N = safe_eval(self.led_delay.text(), self.N, return_type='int') self.led_delay.setText(str(self.N)) if not 'wdg_fil' in fb.fil[0]: fb.fil[0].update({'wdg_fil':{}}) fb.fil[0]['wdg_fil'].update({'delay': {'N':self.N} }) # sig_tx -> select_filter -> filter_specs self.sig_tx.emit({'sender':__name__, 'filt_changed':'delay'}) def _load_dict(self): """ Reload parameter(s) from filter dictionary (if they exist) and set corresponding UI elements. _load_dict() is called upon initialization and when the filter is loaded from disk. """ if 'wdg_fil' in fb.fil[0] and 'delay' in fb.fil[0]['wdg_fil']: wdg_fil_par = fb.fil[0]['wdg_fil']['delay'] if 'N' in wdg_fil_par: self.N = wdg_fil_par['N'] self.led_delay.setText(str(self.N)) def _get_params(self, fil_dict): """ Translate parameters from the passed dictionary to instance parameters, scaling / transforming them if needed. """ self.N = fil_dict['N'] # filter order is translated to numb. of delays def _test_N(self): """ Warn the user if the calculated order is too high for a reasonable filter design. """ if self.N > 2000: return qfilter_warning(self, self.N, "Delay") else: return True def _save(self, fil_dict, arg=None): """ Convert between poles / zeros / gain, filter coefficients (polynomes) and second-order sections and store all available formats in the passed dictionary 'fil_dict'. """ if arg is None: arg = np.zeros(self.N) fil_save(fil_dict, arg, self.FRMT, __name__) def LPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict) def HPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict) def BPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict) def BSman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self._save(fil_dict)
class Plot_Phi(QWidget): # incoming, connected in sender widget (locally connected to self.process_sig_rx() ) sig_rx = pyqtSignal(object) # outgoing, distributed via plot_tab_widget sig_tx = 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.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 dict_sig['sender'] == __name__: logger.debug("Stopped infinite loop\n{0}".format( pprint_log(dict_sig))) return 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.unit_changed) 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.subplots() self.ax.get_xaxis().tick_bottom() # remove axis ticks on top self.ax.get_yaxis().tick_left() # remove axis ticks right #------------------------------------------------------------------------------ def unit_changed(self): """ Unit for phase display has been changed, emit a 'view_changed' signal and continue with drawing. """ self.sig_tx.emit({'sender': __name__, 'view_changed': 'plot_phi'}) self.draw() #------------------------------------------------------------------------------ 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()
class PlotImpz_UI(QWidget): """ Create the UI for the PlotImpz class """ # incoming: not implemented at the moment, update_N is triggered directly # by plot_impz # sig_rx = pyqtSignal(object) # outgoing: from various UI elements to PlotImpz ('ui_changed':'xxx') sig_tx = pyqtSignal(object) # outgoing: to fft related widgets (FFT window widget, qfft_win_select) sig_tx_fft = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from - FFT window widget - qfft_win_select """ # logger.debug("PROCESS_SIG_RX - vis: {0}\n{1}" # .format(self.isVisible(), pprint_log(dict_sig))) if 'id' in dict_sig and dict_sig['id'] == id(self): logger.warning("Stopped infinite loop:\n{0}".format( pprint_log(dict_sig))) return # --- signals coming from the FFT window widget or the FFT window selector if dict_sig['class'] in {'Plot_FFT_win', 'QFFTWinSelector'}: if 'closeEvent' in dict_sig: # hide FFT window widget and return self.hide_fft_wdg() return else: # check for value 'fft_win*': if 'view_changed' in dict_sig and 'fft_win' in dict_sig[ 'view_changed']: # local connection to FFT window widget and qfft_win_select self.emit(dict_sig, sig_name='sig_tx_fft') # global connection to e.g. plot_impz self.emit(dict_sig) # ------------------------------------------------------------------------------ def __init__(self): super().__init__() """ Intitialize the widget, consisting of: - top chkbox row - coefficient table - two bottom rows with action buttons """ # initial settings self.N_start = 0 self.N_user = 0 self.N = 0 self.N_frame_user = 0 self.N_frame = 0 # time self.plt_time_resp = "stem" self.plt_time_stim = "line" self.plt_time_stmq = "none" self.plt_time_spgr = "none" self.bottom_t = -80 # initial value for log. scale (time) self.time_nfft_spgr = 256 # number of fft points per spectrogram segment self.time_ovlp_spgr = 128 # number of overlap points between spectrogram segments self.mode_spgr_time = "psd" # frequency self.cmb_freq_display_item = "mag" self.plt_freq_resp = "line" self.plt_freq_stim = "none" self.plt_freq_stmq = "none" self.bottom_f = -120 # initial value for log. scale self.param = None self.f_scale = fb.fil[0]['f_S'] self.t_scale = fb.fil[0]['T_S'] # list of windows that are available for FFT analysis win_names_list = [ "Boxcar", "Rectangular", "Barthann", "Bartlett", "Blackman", "Blackmanharris", "Bohman", "Cosine", "Dolph-Chebyshev", "Flattop", "General Gaussian", "Gauss", "Hamming", "Hann", "Kaiser", "Nuttall", "Parzen", "Slepian", "Triangular", "Tukey" ] self.cur_win_name = "Rectangular" # set initial window type # initialize windows dict with the list above self.win_dict = get_windows_dict(win_names_list=win_names_list, cur_win_name=self.cur_win_name) # instantiate FFT window with default windows dict self.fft_widget = Plot_FFT_win(self, self.win_dict, sym=False, title="pyFDA Spectral Window Viewer") # hide window initially, this is modeless i.e. a non-blocking popup window self.fft_widget.hide() # data / icon / tooltipp (none) for plotting styles self.plot_styles_list = [ ("Plot style"), ("none", QIcon(":/plot_style-none"), "off"), ("dots*", QIcon(":/plot_style-mkr"), "markers only"), ("line", QIcon(":/plot_style-line"), "line"), ("line*", QIcon(":/plot_style-line-mkr"), "line + markers"), ("stem", QIcon(":/plot_style-stem"), "stems"), ("stem*", QIcon(":/plot_style-stem-mkr"), "stems + markers"), ("steps", QIcon(":/plot_style-steps"), "steps"), ("steps*", QIcon(":/plot_style-steps-mkr"), "steps + markers") ] self.cmb_time_spgr_items = [ "<span>Show Spectrogram for selected signal.</span>", ("none", "None", ""), ("xn", "x[n]", "input"), ("xqn", "x_q[n]", "quantized input"), ("yn", "y[n]", "output") ] self.cmb_mode_spgr_time_items = [ "<span>Spectrogram display mode.</span>", ("psd", "PSD", "<span>Power Spectral Density, either per bin or referred to " "<i>f<sub>S</sub></i></span>"), ("magnitude", "Mag.", "Signal magnitude"), ("angle", "Angle", "Phase, wrapped to ± π"), ("phase", "Phase", "Phase (unwrapped)") ] # self.N self.cmb_freq_display_items = [ "<span>Select how to display the spectrum.</span>", ("mag", "Magnitude", "<span>Spectral magnitude</span>"), ("mag_phi", "Mag. / Phase", "<span>Magnitude and phase.</span>"), ("re_im", "Re. / Imag.", "<span>Real and imaginary part of spectrum.</span>") ] self._construct_UI() # self._enable_stim_widgets() self.update_N(emit=False) # also updates window function and win_dict # self._update_noi() def _construct_UI(self): # ----------- --------------------------------------------------- # Run control widgets # --------------------------------------------------------------- # self.but_auto_run = QPushButtonRT(text=to_html("Auto", frmt="b"), margin=0) self.but_auto_run = QPushButton(" Auto", self) self.but_auto_run.setObjectName("but_auto_run") self.but_auto_run.setToolTip( "<span>Update response automatically when " "parameters have been changed.</span>") # self.but_auto_run.setMaximumWidth(qtext_width(text=" Auto ")) self.but_auto_run.setCheckable(True) self.but_auto_run.setChecked(True) but_height = self.but_auto_run.sizeHint().height() self.but_run = QPushButton(self) self.but_run.setIcon(QIcon(":/play.svg")) self.but_run.setIconSize(QSize(but_height, but_height)) self.but_run.setFixedSize(QSize(2 * but_height, but_height)) self.but_run.setToolTip("Run simulation") self.but_run.setEnabled(True) self.cmb_sim_select = QComboBox(self) self.cmb_sim_select.addItems(["Float", "Fixpoint"]) qset_cmb_box(self.cmb_sim_select, "Float") self.cmb_sim_select.setToolTip("<span>Simulate floating-point or " "fixpoint response.</span>") self.lbl_N_points = QLabel(to_html("N", frmt='bi') + " =", self) self.led_N_points = QLineEdit(self) self.led_N_points.setText(str(self.N)) self.led_N_points.setToolTip( "<span>Last data point. " "<i>N</i> = 0 tries to choose for you.</span>") self.led_N_points.setMaximumWidth(qtext_width(N_x=8)) self.lbl_N_start = QLabel(to_html("N_0", frmt='bi') + " =", self) self.led_N_start = QLineEdit(self) self.led_N_start.setText(str(self.N_start)) self.led_N_start.setToolTip("<span>First point to plot.</span>") self.led_N_start.setMaximumWidth(qtext_width(N_x=8)) self.lbl_N_frame = QLabel(to_html("ΔN", frmt='bi') + " =", self) self.led_N_frame = QLineEdit(self) self.led_N_frame.setText(str(self.N_frame)) self.led_N_frame.setToolTip( "<span>Frame length; longer frames calculate faster but calculation cannot " "be stopped so quickly. " "<i>ΔN</i> = 0 calculates all samples in one frame.</span>") self.led_N_frame.setMaximumWidth(qtext_width(N_x=8)) self.prg_wdg = QProgressBar(self) self.prg_wdg.setFixedHeight(but_height) self.prg_wdg.setFixedWidth(qtext_width(N_x=6)) self.prg_wdg.setMinimum(0) self.prg_wdg.setValue(0) self.but_toggle_stim_options = PushButton(" Stimuli ", checked=True) self.but_toggle_stim_options.setObjectName("but_stim_options") self.but_toggle_stim_options.setToolTip( "<span>Show / hide stimulus options.</span>") self.lbl_stim_cmplx_warn = QLabel(self) self.lbl_stim_cmplx_warn = QLabel(to_html("Cmplx!", frmt='b'), self) self.lbl_stim_cmplx_warn.setToolTip( '<span>Signal is complex valued; ' 'single-sided and H<sub>id</sub> spectra may be wrong.</span>') self.lbl_stim_cmplx_warn.setStyleSheet("background-color : yellow;" "border : 1px solid grey") self.but_fft_wdg = QPushButton(self) self.but_fft_wdg.setIcon(QIcon(":/fft.svg")) self.but_fft_wdg.setIconSize(QSize(but_height, but_height)) self.but_fft_wdg.setFixedSize(QSize(int(1.5 * but_height), but_height)) self.but_fft_wdg.setToolTip( '<span>Show / hide FFT widget (select window type ' ' and display its properties).</span>') self.but_fft_wdg.setCheckable(True) self.but_fft_wdg.setChecked(False) self.qfft_win_select = QFFTWinSelector(self, self.win_dict) self.but_fx_scale = PushButton(" FX:Int ") self.but_fx_scale.setObjectName("but_fx_scale") self.but_fx_scale.setToolTip( "<span>Display data with integer (fixpoint) scale.</span>") self.but_fx_range = PushButton(" FX:Range") self.but_fx_range.setObjectName("but_fx_limits") self.but_fx_range.setToolTip( "<span>Display limits of fixpoint range.</span>") layH_ctrl_run = QHBoxLayout() layH_ctrl_run.addWidget(self.but_auto_run) layH_ctrl_run.addWidget(self.but_run) layH_ctrl_run.addWidget(self.cmb_sim_select) layH_ctrl_run.addSpacing(10) layH_ctrl_run.addWidget(self.lbl_N_start) layH_ctrl_run.addWidget(self.led_N_start) layH_ctrl_run.addWidget(self.lbl_N_points) layH_ctrl_run.addWidget(self.led_N_points) layH_ctrl_run.addWidget(self.lbl_N_frame) layH_ctrl_run.addWidget(self.led_N_frame) layH_ctrl_run.addWidget(self.prg_wdg) layH_ctrl_run.addSpacing(20) layH_ctrl_run.addWidget(self.but_toggle_stim_options) layH_ctrl_run.addSpacing(5) layH_ctrl_run.addWidget(self.lbl_stim_cmplx_warn) layH_ctrl_run.addSpacing(20) layH_ctrl_run.addWidget(self.but_fft_wdg) layH_ctrl_run.addWidget(self.qfft_win_select) layH_ctrl_run.addSpacing(20) layH_ctrl_run.addWidget(self.but_fx_scale) layH_ctrl_run.addWidget(self.but_fx_range) layH_ctrl_run.addStretch(10) # layH_ctrl_run.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_run = QWidget(self) self.wdg_ctrl_run.setLayout(layH_ctrl_run) # --- end of run control ---------------------------------------- # ----------- --------------------------------------------------- # Controls for time domain # --------------------------------------------------------------- self.lbl_plt_time_stim = QLabel(to_html("Stim. x", frmt='bi'), self) self.cmb_plt_time_stim = QComboBox(self) qcmb_box_populate(self.cmb_plt_time_stim, self.plot_styles_list, self.plt_time_stim) self.cmb_plt_time_stim.setToolTip( "<span>Plot style for stimulus.</span>") self.lbl_plt_time_stmq = QLabel( to_html(" Fixp. Stim. x_Q", frmt='bi'), self) self.cmb_plt_time_stmq = QComboBox(self) qcmb_box_populate(self.cmb_plt_time_stmq, self.plot_styles_list, self.plt_time_stmq) self.cmb_plt_time_stmq.setToolTip( "<span>Plot style for <em>fixpoint</em> " "(quantized) stimulus.</span>") lbl_plt_time_resp = QLabel(to_html(" Resp. y", frmt='bi'), self) self.cmb_plt_time_resp = QComboBox(self) qcmb_box_populate(self.cmb_plt_time_resp, self.plot_styles_list, self.plt_time_resp) self.cmb_plt_time_resp.setToolTip( "<span>Plot style for response.</span>") self.lbl_win_time = QLabel(to_html(" Win", frmt='bi'), self) self.chk_win_time = QCheckBox(self) self.chk_win_time.setObjectName("chk_win_time") self.chk_win_time.setToolTip( '<span>Plot FFT windowing function.</span>') self.chk_win_time.setChecked(False) line1 = QVLine() line2 = QVLine(width=5) self.but_log_time = PushButton(" dB") self.but_log_time.setObjectName("but_log_time") self.but_log_time.setToolTip( "<span>Logarithmic scale for y-axis.</span>") lbl_plt_time_spgr = QLabel(to_html("Spectrogram", frmt='bi'), self) self.cmb_plt_time_spgr = QComboBox(self) qcmb_box_populate(self.cmb_plt_time_spgr, self.cmb_time_spgr_items, self.plt_time_spgr) spgr_en = self.plt_time_spgr != "none" self.cmb_mode_spgr_time = QComboBox(self) qcmb_box_populate(self.cmb_mode_spgr_time, self.cmb_mode_spgr_time_items, self.mode_spgr_time) self.cmb_mode_spgr_time.setVisible(spgr_en) self.lbl_byfs_spgr_time = QLabel(to_html(" per f_S", frmt='b'), self) self.lbl_byfs_spgr_time.setVisible(spgr_en) self.chk_byfs_spgr_time = QCheckBox(self) self.chk_byfs_spgr_time.setObjectName("chk_log_spgr") self.chk_byfs_spgr_time.setToolTip("<span>Display spectral density " "i.e. scale by f_S</span>") self.chk_byfs_spgr_time.setChecked(True) self.chk_byfs_spgr_time.setVisible(spgr_en) self.but_log_spgr_time = QPushButton("dB") self.but_log_spgr_time.setMaximumWidth(qtext_width(text=" dB")) self.but_log_spgr_time.setObjectName("but_log_spgr") self.but_log_spgr_time.setToolTip( "<span>Logarithmic scale for spectrogram.</span>") self.but_log_spgr_time.setCheckable(True) self.but_log_spgr_time.setChecked(True) self.but_log_spgr_time.setVisible(spgr_en) self.lbl_time_nfft_spgr = QLabel(to_html(" N_FFT =", frmt='bi'), self) self.lbl_time_nfft_spgr.setVisible(spgr_en) self.led_time_nfft_spgr = QLineEdit(self) self.led_time_nfft_spgr.setText(str(self.time_nfft_spgr)) self.led_time_nfft_spgr.setToolTip("<span>Number of FFT points per " "spectrogram segment.</span>") self.led_time_nfft_spgr.setVisible(spgr_en) self.lbl_time_ovlp_spgr = QLabel(to_html(" N_OVLP =", frmt='bi'), self) self.lbl_time_ovlp_spgr.setVisible(spgr_en) self.led_time_ovlp_spgr = QLineEdit(self) self.led_time_ovlp_spgr.setText(str(self.time_ovlp_spgr)) self.led_time_ovlp_spgr.setToolTip( "<span>Number of overlap data points " "between spectrogram segments.</span>") self.led_time_ovlp_spgr.setVisible(spgr_en) self.lbl_log_bottom_time = QLabel(to_html("min =", frmt='bi'), self) self.led_log_bottom_time = QLineEdit(self) self.led_log_bottom_time.setText(str(self.bottom_t)) self.led_log_bottom_time.setMaximumWidth(qtext_width(N_x=8)) self.led_log_bottom_time.setToolTip( "<span>Minimum display value for time and spectrogram plots with log. scale." "</span>") self.lbl_log_bottom_time.setVisible( self.but_log_time.isChecked() or (spgr_en and self.but_log_spgr_time.isChecked())) self.led_log_bottom_time.setVisible( self.lbl_log_bottom_time.isVisible()) # self.lbl_colorbar_time = QLabel(to_html(" Col.bar", frmt='b'), self) # self.lbl_colorbar_time.setVisible(spgr_en) # self.chk_colorbar_time = QCheckBox(self) # self.chk_colorbar_time.setObjectName("chk_colorbar_time") # self.chk_colorbar_time.setToolTip("<span>Enable colorbar</span>") # self.chk_colorbar_time.setChecked(True) # self.chk_colorbar_time.setVisible(spgr_en) layH_ctrl_time = QHBoxLayout() layH_ctrl_time.addWidget(self.lbl_plt_time_stim) layH_ctrl_time.addWidget(self.cmb_plt_time_stim) # layH_ctrl_time.addWidget(self.lbl_plt_time_stmq) layH_ctrl_time.addWidget(self.cmb_plt_time_stmq) # layH_ctrl_time.addWidget(lbl_plt_time_resp) layH_ctrl_time.addWidget(self.cmb_plt_time_resp) # layH_ctrl_time.addWidget(self.lbl_win_time) layH_ctrl_time.addWidget(self.chk_win_time) layH_ctrl_time.addSpacing(5) layH_ctrl_time.addWidget(line1) layH_ctrl_time.addSpacing(5) # layH_ctrl_time.addWidget(self.lbl_log_bottom_time) layH_ctrl_time.addWidget(self.led_log_bottom_time) layH_ctrl_time.addWidget(self.but_log_time) layH_ctrl_time.addSpacing(5) layH_ctrl_time.addWidget(line2) layH_ctrl_time.addSpacing(5) # layH_ctrl_time.addWidget(lbl_plt_time_spgr) layH_ctrl_time.addWidget(self.cmb_plt_time_spgr) layH_ctrl_time.addWidget(self.cmb_mode_spgr_time) layH_ctrl_time.addWidget(self.lbl_byfs_spgr_time) layH_ctrl_time.addWidget(self.chk_byfs_spgr_time) layH_ctrl_time.addWidget(self.but_log_spgr_time) layH_ctrl_time.addWidget(self.lbl_time_nfft_spgr) layH_ctrl_time.addWidget(self.led_time_nfft_spgr) layH_ctrl_time.addWidget(self.lbl_time_ovlp_spgr) layH_ctrl_time.addWidget(self.led_time_ovlp_spgr) layH_ctrl_time.addStretch(10) # layH_ctrl_time.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_time = QWidget(self) self.wdg_ctrl_time.setLayout(layH_ctrl_time) # ---- end time domain ------------------ # --------------------------------------------------------------- # Controls for frequency domain # --------------------------------------------------------------- self.lbl_plt_freq_stim = QLabel(to_html("Stimulus X", frmt='bi'), self) self.cmb_plt_freq_stim = QComboBox(self) qcmb_box_populate(self.cmb_plt_freq_stim, self.plot_styles_list, self.plt_freq_stim) self.cmb_plt_freq_stim.setToolTip( "<span>Plot style for stimulus.</span>") self.lbl_plt_freq_stmq = QLabel( to_html(" Fixp. Stim. X_Q", frmt='bi'), self) self.cmb_plt_freq_stmq = QComboBox(self) qcmb_box_populate(self.cmb_plt_freq_stmq, self.plot_styles_list, self.plt_freq_stmq) self.cmb_plt_freq_stmq.setToolTip( "<span>Plot style for <em>fixpoint</em> (quantized) stimulus.</span>" ) lbl_plt_freq_resp = QLabel(to_html(" Response Y", frmt='bi'), self) self.cmb_plt_freq_resp = QComboBox(self) qcmb_box_populate(self.cmb_plt_freq_resp, self.plot_styles_list, self.plt_freq_resp) self.cmb_plt_freq_resp.setToolTip( "<span>Plot style for response.</span>") self.but_log_freq = QPushButton("dB") self.but_log_freq.setMaximumWidth(qtext_width(" dB")) self.but_log_freq.setObjectName(".but_log_freq") self.but_log_freq.setToolTip( "<span>Logarithmic scale for y-axis.</span>") self.but_log_freq.setCheckable(True) self.but_log_freq.setChecked(True) self.lbl_log_bottom_freq = QLabel(to_html("min =", frmt='bi'), self) self.lbl_log_bottom_freq.setVisible(self.but_log_freq.isChecked()) self.led_log_bottom_freq = QLineEdit(self) self.led_log_bottom_freq.setText(str(self.bottom_f)) self.led_log_bottom_freq.setMaximumWidth(qtext_width(N_x=8)) self.led_log_bottom_freq.setToolTip( "<span>Minimum display value for log. scale.</span>") self.led_log_bottom_freq.setVisible(self.but_log_freq.isChecked()) if not self.but_log_freq.isChecked(): self.bottom_f = 0 self.cmb_freq_display = QComboBox(self) qcmb_box_populate(self.cmb_freq_display, self.cmb_freq_display_items, self.cmb_freq_display_item) self.cmb_freq_display.setObjectName("cmb_re_im_freq") self.but_Hf = QPushButtonRT(self, to_html("H_id", frmt="bi"), margin=5) self.but_Hf.setObjectName("chk_Hf") self.but_Hf.setToolTip( "<span>Show ideal frequency response, calculated " "from the filter coefficients.</span>") self.but_Hf.setChecked(False) self.but_Hf.setCheckable(True) self.but_freq_norm_impz = QPushButtonRT( text="<b><i>E<sub>X</sub></i> = 1</b>", margin=5) self.but_freq_norm_impz.setToolTip( "<span>Normalize the FFT of the stimulus with <i>N<sub>FFT</sub></i> for " "<i>E<sub>X</sub></i> = 1. For a dirac pulse, this yields " "|<i>Y(f)</i>| = |<i>H(f)</i>|. DC and Noise need to be " "turned off, window should be <b>Rectangular</b>.</span>") self.but_freq_norm_impz.setCheckable(True) self.but_freq_norm_impz.setChecked(True) self.but_freq_norm_impz.setObjectName("freq_norm_impz") self.but_freq_show_info = QPushButton("Info", self) self.but_freq_show_info.setMaximumWidth(qtext_width(" Info ")) self.but_freq_show_info.setObjectName("but_show_info_freq") self.but_freq_show_info.setToolTip( "<span>Show signal power in legend.</span>") self.but_freq_show_info.setCheckable(True) self.but_freq_show_info.setChecked(False) layH_ctrl_freq = QHBoxLayout() layH_ctrl_freq.addWidget(self.lbl_plt_freq_stim) layH_ctrl_freq.addWidget(self.cmb_plt_freq_stim) # layH_ctrl_freq.addWidget(self.lbl_plt_freq_stmq) layH_ctrl_freq.addWidget(self.cmb_plt_freq_stmq) # layH_ctrl_freq.addWidget(lbl_plt_freq_resp) layH_ctrl_freq.addWidget(self.cmb_plt_freq_resp) # layH_ctrl_freq.addSpacing(5) layH_ctrl_freq.addWidget(self.but_Hf) layH_ctrl_freq.addStretch(1) # layH_ctrl_freq.addWidget(self.lbl_log_bottom_freq) layH_ctrl_freq.addWidget(self.led_log_bottom_freq) layH_ctrl_freq.addWidget(self.but_log_freq) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(self.cmb_freq_display) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(self.but_freq_norm_impz) layH_ctrl_freq.addStretch(1) layH_ctrl_freq.addWidget(self.but_freq_show_info) layH_ctrl_freq.addStretch(10) # layH_ctrl_freq.setContentsMargins(*params['wdg_margins']) self.wdg_ctrl_freq = QWidget(self) self.wdg_ctrl_freq.setLayout(layH_ctrl_freq) # ---- end Frequency Domain ------------------ # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- # connect FFT widget to qfft_selector and vice versa and to and signals upstream: self.fft_widget.sig_tx.connect(self.process_sig_rx) self.qfft_win_select.sig_tx.connect(self.process_sig_rx) # connect process_sig_rx output to both FFT widgets self.sig_tx_fft.connect(self.fft_widget.sig_rx) self.sig_tx_fft.connect(self.qfft_win_select.sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- # --- run control --- self.led_N_start.editingFinished.connect(self.update_N) self.led_N_points.editingFinished.connect(self.update_N) self.led_N_frame.editingFinished.connect(self.update_N) self.but_fft_wdg.clicked.connect(self.toggle_fft_wdg) # ------------------------------------------------------------------------- def update_N(self, emit=True): """ Update values for `self.N` and `self.win_dict['N']`, for `self.N_start` and `self.N_end` from the corresponding QLineEditWidgets. When `emit==True`, fire `'ui_changed': 'N'` to update the FFT window and the `plot_impz` widgets. In contrast to `view_changed`, this also forces a recalculation of the transient response. This method is called by: - `self._construct_ui()` with `emit==False` - `plot_impz()` with `emit==False` when the automatic calculation of N has to be updated (e.g. order of FIR Filter has changed - signal-slot connection when `N_start` or `N_end` QLineEdit widgets have been changed (`emit==True`) """ if not isinstance(emit, bool): logger.error("update N: emit={0}".format(emit)) self.N_start = safe_eval(self.led_N_start.text(), self.N_start, return_type='int', sign='poszero') self.led_N_start.setText(str(self.N_start)) # update widget self.N_user = safe_eval(self.led_N_points.text(), self.N_user, return_type='int', sign='poszero') if self.N_user == 0: # automatic calculation self.N = self.calc_n_points(self.N_user) # widget remains set to 0 self.led_N_points.setText("0") # update widget else: self.N = self.N_user self.led_N_points.setText(str(self.N)) # update widget # total number of points to be calculated: N + N_start self.N_end = self.N + self.N_start self.N_frame_user = safe_eval(self.led_N_frame.text(), self.N_frame_user, return_type='int', sign='poszero') if self.N_frame_user == 0: self.N_frame = self.N_end # use N_end for frame length self.led_N_frame.setText( "0") # update widget with "0" as set by user else: self.N_frame = self.N_frame_user self.led_N_frame.setText(str(self.N_frame)) # update widget # recalculate displayed freq. index values when freq. unit == 'k' if fb.fil[0]['freq_specs_unit'] == 'k': self.update_freqs() if emit: # use `'ui_changed'` as this triggers recalculation of the transient # response self.emit({'ui_changed': 'N'}) # ------------------------------------------------------------------------------ def toggle_fft_wdg(self): """ Show / hide FFT widget depending on the state of the corresponding button When widget is shown, trigger an update of the window function. """ if self.but_fft_wdg.isChecked(): self.fft_widget.show() self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_fft') else: self.fft_widget.hide() # -------------------------------------------------------------------------- def hide_fft_wdg(self): """ The closeEvent caused by clicking the "x" in the FFT widget is caught there and routed here to only hide the window """ self.but_fft_wdg.setChecked(False) self.fft_widget.hide() # ------------------------------------------------------------------------------ def calc_n_points(self, N_user=0): """ Calculate number of points to be displayed, depending on type of filter (FIR, IIR) and user input. If the user selects 0 points, the number is calculated automatically. An improvement would be to calculate the dominant pole and the corresponding settling time. """ if N_user == 0: # set number of data points automatically if fb.fil[0]['ft'] == 'IIR': # IIR: No algorithm yet, set N = 100 N = 100 else: # FIR: N = number of coefficients (max. 100) N = min(len(fb.fil[0]['ba'][0]), 100) else: N = N_user return N
class Input_Coeffs_UI(QWidget): """ Create the UI for the FilterCoeffs class """ sig_rx = pyqtSignal(dict) # incoming sig_tx = pyqtSignal(dict) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(Input_Coeffs_UI, self).__init__(parent) self.eps = 1.e-6 # initialize tolerance value self._construct_UI() # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process signals coming from the CSV pop-up window """ # logger.debug("PROCESS_SIG_RX:\n{0}".format(pprint_log(dict_sig))) if 'closeEvent' in dict_sig: self._close_csv_win() self.emit({'ui_changed': 'csv'}) return elif 'ui_changed' in dict_sig: self._set_load_save_icons() # update icons file <-> clipboard # inform e.g. the p/z input widget about changes in CSV options self.emit({'ui_changed': 'csv'}) # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget, consisting of: - top chkbox row - coefficient table - two bottom rows with action buttons """ self.bfont = QFont() self.bfont.setBold(True) self.bifont = QFont() self.bifont.setBold(True) self.bifont.setItalic(True) # q_icon_size = QSize(20, 20) # optional, size is derived from butEnable ####################################################################### # frmMain # # This frame contains all the buttons ####################################################################### # --------------------------------------------- # layHDisplay # # UI Elements for controlling the display # --------------------------------------------- self.butEnable = PushButton(self, icon=QIcon(':/circle-check.svg'), checked=True) q_icon_size = self.butEnable.iconSize() # <- uncomment this for manual sizing self.butEnable.setToolTip( "<span>Show / hide filter coefficients in an editable table." " For high order systems, table display might be slow.</span>") fix_formats = ['Dec', 'Hex', 'Bin', 'CSD'] self.cmbFormat = QComboBox(self) model = self.cmbFormat.model() item = QtGui.QStandardItem('Float') item.setData('child', Qt.AccessibleDescriptionRole) model.appendRow(item) item = QtGui.QStandardItem('Fixp.:') item.setData('parent', Qt.AccessibleDescriptionRole) item.setData(0, QtGui.QFont.Bold) item.setFlags(item.flags() & ~Qt.ItemIsEnabled) # | Qt.ItemIsSelectable)) model.appendRow(item) for idx in range(len(fix_formats)): item = QtGui.QStandardItem(fix_formats[idx]) # item.setForeground(QtGui.QColor('red')) model.appendRow(item) self.cmbFormat.insertSeparator(1) qset_cmb_box(self.cmbFormat, 'float') self.cmbFormat.setToolTip('Set the display format.') self.cmbFormat.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.spnDigits = QSpinBox(self) self.spnDigits.setRange(0, 16) self.spnDigits.setValue(params['FMT_ba']) self.spnDigits.setToolTip("Number of digits to display.") self.lblDigits = QLabel("Digits", self) self.lblDigits.setFont(self.bifont) self.cmbQFrmt = QComboBox(self) q_formats = [('Norm. Frac.', 'qnfrac'), ('Integer', 'qint'), ('Fractional', 'qfrac')] for q in q_formats: self.cmbQFrmt.addItem(*q) self.lbl_W = QLabel("W = ", self) self.lbl_W.setFont(self.bifont) self.ledW = QLineEdit(self) self.ledW.setToolTip("Specify total wordlength.") self.ledW.setText("16") self.ledW.setMaxLength(2) # maximum of 2 digits self.ledW.setFixedWidth(30) # width of lineedit in points(?) layHDisplay = QHBoxLayout() layHDisplay.setAlignment(Qt.AlignLeft) layHDisplay.addWidget(self.butEnable) layHDisplay.addWidget(self.cmbFormat) layHDisplay.addWidget(self.spnDigits) layHDisplay.addWidget(self.lblDigits) layHDisplay.addWidget(self.cmbQFrmt) layHDisplay.addWidget(self.lbl_W) layHDisplay.addWidget(self.ledW) layHDisplay.addStretch() ####################################################################### # frmButtonsCoeffs # # This frame contains all buttons for manipulating coefficients ####################################################################### # ----------------------------------------------------------------- # layHButtonsCoeffs1 # # UI Elements for loading / storing / manipulating cells and rows # ----------------------------------------------------------------- self.cmbFilterType = QComboBox(self) self.cmbFilterType.setObjectName("comboFilterType") self.cmbFilterType.setToolTip( "<span>Select between IIR and FIR filter for manual entry." "Changing the type reloads the filter from the filter dict.</span>") self.cmbFilterType.addItems(["FIR", "IIR"]) self.cmbFilterType.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butAddCells = QPushButton(self) self.butAddCells.setIcon(QIcon(':/row_insert_above.svg')) self.butAddCells.setIconSize(q_icon_size) self.butAddCells.setToolTip( "<span>Select cells to insert a new cell above each selected cell. " "Use <SHIFT> or <CTRL> to select multiple cells. " "When nothing is selected, add a row at the end.</span>") self.butDelCells = QPushButton(self) self.butDelCells.setIcon(QIcon(':/row_delete.svg')) self.butDelCells.setIconSize(q_icon_size) self.butDelCells.setToolTip( "<span>Delete selected cell(s) from the table. " "Use <SHIFT> or <CTRL> to select multiple cells. " "When nothing is selected, delete the last row.</span>") self.butSave = QPushButton(self) self.butSave.setIcon(QIcon(':/upload.svg')) self.butSave.setIconSize(q_icon_size) self.butSave.setToolTip( "<span>Copy coefficient table to filter dict and update all plots" "and widgets.</span>") self.butLoad = QPushButton(self) self.butLoad.setIcon(QIcon(':/download.svg')) self.butLoad.setIconSize(q_icon_size) self.butLoad.setToolTip("Reload coefficient table from filter dict.") self.butClear = QPushButton(self) self.butClear.setIcon(QIcon(':/trash.svg')) self.butClear.setIconSize(q_icon_size) self.butClear.setToolTip("Clear all table entries.") self.butFromTable = QPushButton(self) self.butFromTable.setIconSize(q_icon_size) self.butToTable = QPushButton(self) self.butToTable.setIconSize(q_icon_size) self.but_csv_options = PushButton(self, icon=QIcon(':/settings.svg'), checked=False) self.but_csv_options.setIconSize(q_icon_size) self.but_csv_options.setToolTip( "<span>Select CSV format and whether " "to copy to/from clipboard or file.</span>") self._set_load_save_icons() # initialize icon / button settings layHButtonsCoeffs1 = QHBoxLayout() layHButtonsCoeffs1.addWidget(self.cmbFilterType) layHButtonsCoeffs1.addWidget(self.butAddCells) layHButtonsCoeffs1.addWidget(self.butDelCells) layHButtonsCoeffs1.addWidget(self.butClear) layHButtonsCoeffs1.addWidget(self.butSave) layHButtonsCoeffs1.addWidget(self.butLoad) layHButtonsCoeffs1.addWidget(self.butFromTable) layHButtonsCoeffs1.addWidget(self.butToTable) layHButtonsCoeffs1.addWidget(self.but_csv_options) layHButtonsCoeffs1.addStretch() # ---------------------------------------------------------------------- # layHButtonsCoeffs2 # # Eps / set zero settings # --------------------------------------------------------------------- self.butSetZero = QPushButton("= 0", self) self.butSetZero.setToolTip( "<span>Set selected coefficients = 0 with a magnitude < ε. " "When nothing is selected, test the whole table.</span>") self.butSetZero.setIconSize(q_icon_size) lblEps = QLabel(self) lblEps.setText("<b><i>for b, a</i> <</b>") self.ledEps = QLineEdit(self) self.ledEps.setToolTip("Specify tolerance value.") layHButtonsCoeffs2 = QHBoxLayout() layHButtonsCoeffs2.addWidget(self.butSetZero) layHButtonsCoeffs2.addWidget(lblEps) layHButtonsCoeffs2.addWidget(self.ledEps) layHButtonsCoeffs2.addStretch() # ------------------------------------------------------------------- # Now put the ButtonsCoeffs HBoxes into frmButtonsCoeffs # --------------------------------------------------------------------- layVButtonsCoeffs = QVBoxLayout() layVButtonsCoeffs.addLayout(layHButtonsCoeffs1) layVButtonsCoeffs.addLayout(layHButtonsCoeffs2) layVButtonsCoeffs.setContentsMargins(0, 5, 0, 0) # This frame encompasses all Quantization Settings self.frmButtonsCoeffs = QFrame(self) self.frmButtonsCoeffs.setLayout(layVButtonsCoeffs) # ###################################################################### # frmQSettings # # This frame contains all quantization settings # ###################################################################### # ------------------------------------------------------------------- # layHW_Scale # # QFormat and scale settings # --------------------------------------------------------------------- lbl_Q = QLabel("Q =", self) lbl_Q.setFont(self.bifont) self.ledWI = QLineEdit(self) self.ledWI.setToolTip("Specify number of integer bits.") self.ledWI.setText("0") self.ledWI.setMaxLength(2) # maximum of 2 digits self.ledWI.setFixedWidth(30) # width of lineedit in points(?) self.lblDot = QLabel(".", self) # class attribute, visibility is toggled self.lblDot.setFont(self.bfont) self.ledWF = QLineEdit(self) self.ledWF.setToolTip("Specify number of fractional bits.") self.ledWF.setText("15") self.ledWF.setMaxLength(2) # maximum of 2 digits # self.ledWF.setFixedWidth(30) # width of lineedit in points(?) self.ledWF.setMaximumWidth(30) self.lblScale = QLabel("<b><i>Scale</i> =</b>", self) self.ledScale = QLineEdit(self) self.ledScale.setToolTip( "Set the scale for converting float to fixpoint representation.") self.ledScale.setText(str(1)) self.ledScale.setEnabled(False) layHWI_WF = QHBoxLayout() layHWI_WF.addWidget(lbl_Q) layHWI_WF.addWidget(self.ledWI) layHWI_WF.addWidget(self.lblDot) layHWI_WF.addWidget(self.ledWF) layHWI_WF.addStretch() layHScale = QHBoxLayout() layHScale.addWidget(self.lblScale) layHScale.addWidget(self.ledScale) layHScale.addStretch() layHW_Scale = QHBoxLayout() layHW_Scale.addLayout(layHWI_WF) layHW_Scale.addLayout(layHScale) # ------------------------------------------------------------------- # layGQOpt # # Quantization / Overflow / MSB / LSB settings # --------------------------------------------------------------------- lblQOvfl = QLabel("Ovfl.:", self) lblQOvfl.setFont(self.bifont) lblQuant = QLabel("Quant.:", self) lblQuant.setFont(self.bifont) self.cmbQOvfl = QComboBox(self) qOvfl = ['wrap', 'sat'] self.cmbQOvfl.addItems(qOvfl) qset_cmb_box(self.cmbQOvfl, 'sat') self.cmbQOvfl.setToolTip("Select overflow behaviour.") # ComboBox size is adjusted automatically to fit the longest element self.cmbQOvfl.setSizeAdjustPolicy(QComboBox.AdjustToContents) layHQOvflOpt = QHBoxLayout() layHQOvflOpt.addWidget(lblQOvfl) layHQOvflOpt.addWidget(self.cmbQOvfl) layHQOvflOpt.addStretch() self.cmbQuant = QComboBox(self) qQuant = ['none', 'round', 'fix', 'floor'] self.cmbQuant.addItems(qQuant) qset_cmb_box(self.cmbQuant, 'round') self.cmbQuant.setToolTip("Select the kind of quantization.") self.cmbQuant.setSizeAdjustPolicy(QComboBox.AdjustToContents) layHQuantOpt = QHBoxLayout() layHQuantOpt.addWidget(lblQuant) layHQuantOpt.addWidget(self.cmbQuant) layHQuantOpt.addStretch() self.butQuant = QPushButton(self) self.butQuant.setToolTip( "<span>Quantize selected coefficients / " "whole table with specified settings.</span>") self.butQuant.setIcon(QIcon(':/quantize.svg')) self.butQuant.setIconSize(q_icon_size) self.butQuant.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) lblMSBtxt = QLabel(self) lblMSBtxt.setText("<b><i>MSB</i><sub>10</sub> =</b>") self.lblMSB = QLabel(self) layHMSB = QHBoxLayout() layHMSB.addWidget(lblMSBtxt) layHMSB.addWidget(self.lblMSB) layHMSB.addStretch() lblLSBtxt = QLabel(self) lblLSBtxt.setText("<b><i>LSB</i><sub>10</sub> =</b>") self.lblLSB = QLabel(self) layHLSB = QHBoxLayout() layHLSB.addWidget(lblLSBtxt) layHLSB.addWidget(self.lblLSB) layHLSB.addStretch() layGQOpt = QGridLayout() layGQOpt.addLayout(layHQOvflOpt, 0, 0) layGQOpt.addLayout(layHQuantOpt, 0, 1) layGQOpt.addWidget(self.butQuant, 0, 2, Qt.AlignCenter) layGQOpt.addLayout(layHMSB, 1, 0) layGQOpt.addLayout(layHLSB, 1, 1) # ------------------------------------------------------------------- # Display MAX # --------------------------------------------------------------------- lblMAXtxt = QLabel(self) lblMAXtxt.setText("<b><i>Max =</i></b>") self.lblMAX = QLabel(self) layHCoeffs_MAX = QHBoxLayout() layHCoeffs_MAX.addWidget(lblMAXtxt) layHCoeffs_MAX.addWidget(self.lblMAX) layHCoeffs_MAX.addStretch() ####################################################################### # Now put all the coefficient HBoxes into frmQSettings # --------------------------------------------------------------------- layVButtonsQ = QVBoxLayout() layVButtonsQ.addLayout(layHW_Scale) layVButtonsQ.addLayout(layGQOpt) layVButtonsQ.addLayout(layHCoeffs_MAX) layVButtonsQ.setContentsMargins(0, 0, 0, 0) # This frame encompasses all Quantization Settings self.frmQSettings = QFrame(self) self.frmQSettings.setLayout(layVButtonsQ) ####################################################################### # ######################## Main UI Layout ############################ ####################################################################### # layout for frame (UI widget) layVMainF = QVBoxLayout() layVMainF.addLayout(layHDisplay) layVMainF.addWidget(self.frmQSettings) layVMainF.addWidget(QHLine()) layVMainF.addWidget(self.frmButtonsCoeffs) # This frame encompasses all UI elements frmMain = QFrame(self) frmMain.setLayout(layVMainF) layVMain = QVBoxLayout() # the following affects only the first widget (intended here) layVMain.setAlignment(Qt.AlignTop) layVMain.addWidget(frmMain) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) ####################################################################### # --- set initial values from dict ------------ self.spnDigits.setValue(params['FMT_ba']) self.ledEps.setText(str(self.eps)) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.but_csv_options.clicked.connect(self._open_csv_win) # -------------------------------------------------------------------------- def _open_csv_win(self): """ Pop-up window for CSV options """ if self.but_csv_options.isChecked(): qstyle_widget(self.but_csv_options, "changed") else: qstyle_widget(self.but_csv_options, "normal") if dirs.csv_options_handle is None: # no handle to the window? Create a new instance if self.but_csv_options.isChecked(): # Important: Handle to window must be class attribute, otherwise it # (and the attached window) is deleted immediately when it goes # out of scope dirs.csv_options_handle = CSV_option_box(self) dirs.csv_options_handle.sig_tx.connect(self.process_sig_rx) dirs.csv_options_handle.show() # modeless i.e. non-blocking popup window else: if not self.but_csv_options.isChecked(): # this should not happen if dirs.csv_options_handle is None: logger.warning("CSV options window is already closed!") else: dirs.csv_options_handle.close() self.emit({'ui_changed': 'csv'}) # ------------------------------------------------------------------------------ def _close_csv_win(self): dirs.csv_options_handle = None self.but_csv_options.setChecked(False) qstyle_widget(self.but_csv_options, "normal") # ------------------------------------------------------------------------------ def _set_load_save_icons(self): """ Set icons / tooltipps for loading and saving data to / from file or clipboard depending on selected options. """ if params['CSV']['clipboard']: self.butFromTable.setIcon(QIcon(':/to_clipboard.svg')) self.butFromTable.setToolTip( "<span>Copy table to clipboard, SELECTED items are copied as " "displayed. When nothing is selected, the whole table " "is copied with full precision in decimal format.</span>") self.butToTable.setIcon(QIcon(':/from_clipboard.svg')) self.butToTable.setToolTip("<span>Copy clipboard to table.</span>") else: self.butFromTable.setIcon(QIcon(':/save.svg')) self.butFromTable.setToolTip( "<span>" "Save table to file, SELECTED items are copied as " "displayed. When nothing is selected, the whole table " "is copied with full precision in decimal format.</span>") self.butToTable.setIcon(QIcon(':/file.svg')) self.butToTable.setToolTip("<span>Load table from file.</span>") if dirs.csv_options_handle is None: qstyle_widget(self.but_csv_options, "normal") self.but_csv_options.setChecked(False) else: qstyle_widget(self.but_csv_options, "changed") self.but_csv_options.setChecked(True)
class MplToolbar(NavigationToolbar): """ Custom Matplotlib Navigationtoolbar, derived (subclassed) from Qt's NavigationToolbar with the following changes: - new icon set - new functions and icons for grid toggle, full view, screenshot - removed buttons for configuring subplots and editing curves - added an x,y location widget and icon Signalling / communication works via the signal `sig_tx' derived from http://www.python-forum.de/viewtopic.php?f=24&t=26437 http://pydoc.net/Python/pyQPCR/0.7/pyQPCR.widgets.matplotlibWidget/ !! http://matplotlib.org/users/navigation_toolbar.html !! see also http://stackoverflow.com/questions/17711099/programmatically-change-matplotlib-toolbar-mode-in-qt4 http://matplotlib-users.narkive.com/C8XwIXah/need-help-with-darren-dale-qt-example-of-extending-toolbar https://sukhbinder.wordpress.com/2013/12/16/simple-pyqt-and-matplotlib-example-with-zoompan/ Changing the info: http://stackoverflow.com/questions/15876011/add-information-to-matplotlib-navigation-toolbar-status-bar https://stackoverflow.com/questions/53099295/matplotlib-navigationtoolbar-advanced-figure-options Using Tool Manager https://matplotlib.org/3.1.1/gallery/user_interfaces/toolmanager_sgskip.html https://stackoverflow.com/questions/52971285/add-toolbar-button-icon-matplotlib """ toolitems = () # remove original icons and actions # toolitems = ( # ('Home', 'Reset original view', 'home', 'home'), # ('Back', 'Back to previous view', 'action-undo', 'back'), # ('Forward', 'Forward to next view', 'action-redo', 'forward'), # (None, None, None, None), # ('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'), # ('Zoom', 'Zoom to rectangle', 'magnifying-glass', 'zoom'), # (None, None, None, None), # ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), # ('Save', 'Save the figure', 'file', 'save_figure'), # ) # subclass NavigationToolbar, passing through arguments: #def __init__(self, canvas, parent, coordinates=True): sig_tx = pyqtSignal(object) # general signal, containing a dict def _init_toolbar(self): pass # needed for backward compatibility with mpl < 3.3 # disable coordinate display when mplcursors is available if MPL_CURS: def set_message(self, msg): pass def __init__(self, canv, mpl_widget, *args, **kwargs): NavigationToolbar.__init__(self, canv, mpl_widget, *args, **kwargs) #self.canvas = canv self.mpl_widget = mpl_widget #------------------------------------------------------------------------------ #---------------- Construct Toolbar using QRC icons ------------------- # ENABLE: # self.a_en = self.addAction(QIcon(':/circle-x.svg'), 'Enable Update', self.enable_plot) # self.a_en.setToolTip('Enable / disable plot') # self.a_en.setCheckable(True) # self.a_en.setChecked(True) ## self.a.setEnabled(False) # self.addSeparator() #--------------------------------------------- #--------------------------------------------- # HOME: #--------------------------------------------- self.a_ho = self.addAction(QIcon(':/home.svg'), 'Home', self.home) self.a_ho.setToolTip('Reset zoom') # BACK: self.a_ba = self.addAction(QIcon(':/action-undo.svg'), 'Back', self.back) self.a_ba.setToolTip('Back to previous zoom') #--------------------------------------------- # FORWARD: #--------------------------------------------- self.a_fw = self.addAction(QIcon(':/action-redo.svg'), 'Forward', self.forward) self.a_fw.setToolTip('Forward to next zoom') #--------------------------------------------- self.addSeparator() #--------------------------------------------- #--------------------------------------------- # PAN: #--------------------------------------------- self.a_pa = self.addAction(QIcon(':/move.svg'), 'Pan', self.pan) self.a_pa.setToolTip("Pan axes with left mouse button, zoom with right,\n" "pressing x / y / CTRL keys constrains to horizontal / vertical / diagonal movements.") self._actions['pan'] = self.a_pa self.a_pa.setCheckable(True) #--------------------------------------------- # ZOOM RECTANGLE: #--------------------------------------------- self.a_zo = self.addAction(QIcon(':/magnifying-glass.svg'), 'Zoom', self.zoom) self.a_zo.setToolTip("Zoom in / out to rectangle with left / right mouse button,\n" "pressing x / y keys constrains zoom to horizontal / vertical direction.") self._actions['zoom'] = self.a_zo self.a_zo.setCheckable(True) #--------------------------------------------- # FULL VIEW: #--------------------------------------------- self.a_fv = self.addAction(QIcon(':/fullscreen-enter.svg'), \ 'Zoom full extent', self.mpl_widget.plt_full_view) self.a_fv.setToolTip('Zoom to full extent') #--------------------------------------------- # LOCK ZOOM: #--------------------------------------------- self.a_lk = self.addAction(QIcon(':/lock-unlocked.svg'), \ 'Lock zoom', self.toggle_lock_zoom) self.a_lk.setCheckable(True) self.a_lk.setChecked(False) self.a_lk.setToolTip('Lock / unlock current zoom setting') #--------------------------------------------- # TRACKING CURSOR: #--------------------------------------------- if MPL_CURS: self.a_cr = self.addAction(QIcon(':/map-marker.svg'), \ 'Cursor', self.mpl_widget.toggle_cursor) self.a_cr.setCheckable(True) self.a_cr.setChecked(False) self.a_cr.setToolTip('Tracking Cursor') # -------------------------------------- self.addSeparator() # -------------------------------------- #--------------------------------------------- # GRID: #--------------------------------------------- self.a_gr = self.addAction(QIcon(':/grid_coarse.svg'), 'Grid', self.cycle_draw_grid) self.a_gr.setToolTip('Cycle grid: Off / coarse / fine') self.a_gr_state = 2 # 0: off, 1: major, 2: minor #--------------------------------------------- # REDRAW: #--------------------------------------------- #self.a_rd = self.addAction(QIcon(':/brush.svg'), 'Redraw', self.mpl_widget.redraw) #self.a_rd.setToolTip('Redraw Plot') # -------------------------------------- # SAVE: # -------------------------------------- self.a_sv = self.addAction(QIcon(':/save.svg'), 'Save', self.save_figure) self.a_sv.setToolTip('Save the figure') self.cb = fb.clipboard self.a_cb = self.addAction(QIcon(':/clipboard.svg'), 'To Clipboard', self.mpl2Clip) self.a_cb.setToolTip('Copy to clipboard in png format.') self.a_cb.setShortcut("Ctrl+C") # -------------------------------------- self.addSeparator() # -------------------------------------- # -------------------------------------- # SETTINGS: # -------------------------------------- if figureoptions is not None: self.a_op = self.addAction(QIcon(':/settings.svg'), 'Customize', self.edit_parameters) self.a_op.setToolTip('Edit curves line and axes parameters') # self.buttons = {} # -------------------------------------- # PRINT COORDINATES (only when mplcursors is not available): # -------------------------------------- # Add the x,y location widget at the right side of the toolbar # The stretch factor is 1 which means any resizing of the toolbar # will resize this label instead of the buttons. # -------------------------------------- if not MPL_CURS and self.coordinates: self.addSeparator() self.locLabel = QLabel("", self) self.locLabel.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) self.locLabel.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)) labelAction = self.addWidget(self.locLabel) labelAction.setVisible(True) #--------------------------------------------- # HELP: #--------------------------------------------- self.a_he = self.addAction(QIcon(':/help.svg'), 'help', self.help) self.a_he.setToolTip('Open help page from https://pyfda.rtfd.org in browser') self.a_he.setDisabled(True) #------------------------------------------------------------------------------ if figureoptions is not None: def edit_parameters(self): allaxes = self.canvas.figure.get_axes() if len(allaxes) == 1: axes = allaxes[0] else: titles = [] for axes in allaxes: title = axes.get_title() ylabel = axes.get_ylabel() label = axes.get_label() if title: fmt = "%(title)s" if ylabel: fmt += ": %(ylabel)s" fmt += " (%(axes_repr)s)" elif ylabel: fmt = "%(axes_repr)s (%(ylabel)s)" elif label: fmt = "%(axes_repr)s (%(label)s)" else: fmt = "%(axes_repr)s" titles.append(fmt % dict(title=title, ylabel=ylabel, label=label, axes_repr=repr(axes))) item, ok = QInputDialog.getItem( self, 'Customize', 'Select axes:', titles, 0, False) if ok: axes = allaxes[titles.index(str(item))] else: return figureoptions.figure_edit(axes, self) #------------------------------------------------------------------------------ def home(self): """ Reset zoom to default settings (defined by plotting widget). This method shadows `home()` inherited from NavigationToolbar. """ self.push_current() self.sig_tx.emit({'sender':__name__, 'home':''}) # only the key is used by the slot self.mpl_widget.redraw() #------------------------------------------------------------------------------ def help(self): """ Open help page from https://pyfda.rtfd.org in browser """ url = QtCore.QUrl('https://pyfda.readthedocs.io/en/latest/' + self.a_he.info) if not url.isValid(): logger.warning("Invalid URL\n\t{0}\n\tOpening " "'https://pyfda.readthedocs.io/en/latest/' instead".format(url.toString())) url = QtCore.QUrl('https://pyfda.readthedocs.io/en/latest/') #if url.isLocalFile() QtGui.QDesktopServices.openUrl(url) #https://stackoverflow.com/questions/28494571/how-in-qt5-to-check-if-url-is-available #https://stackoverflow.com/questions/16778435/python-check-if-website-exists #------------------------------------------------------------------------------ def cycle_draw_grid(self, cycle=True, axes=None): """ Cycle the grid of all axes through the states 'off', 'coarse' and 'fine' and redraw the figure. Parameters ---------- cycle : bool, optional Cycle the grid display and redraw the canvas in the end when True. When false, only restore the grid settings. axes : matplotlib axes, optional When none is passed, use local `self.mpl_widget.fig.axes` Returns ------- None. """ if cycle: self.a_gr_state = (self.a_gr_state + 1) % 3 if not axes: axes = self.mpl_widget.fig.axes for ax in self.mpl_widget.fig.axes: if hasattr(ax, "is_twin"): # the axis is a twinx() system, suppress the gridlines ax.grid(False) else: if self.a_gr_state == 0: ax.grid(False, which='both') self.a_gr.setIcon(QIcon(':/grid_none.svg')) elif self.a_gr_state == 1: ax.grid(True, which='major', lw=0.75, ls='-') ax.grid(False, which='minor') self.a_gr.setIcon(QIcon(':/grid_coarse.svg')) else: ax.grid(True, which='major', lw=0.75, ls='-') ax.grid(True, which='minor') self.a_gr.setIcon(QIcon(':/grid_fine.svg')) if cycle: self.canvas.draw() # don't use self.draw(), use FigureCanvasQTAgg.draw() #------------------------------------------------------------------------------ def toggle_lock_zoom(self): """ Toggle the lock zoom settings and save the plot limits in any case: when previously unlocked, settings need to be saved when previously locked, current settings can be saved without effect """ self.mpl_widget.save_limits() # save limits in any case: self.zoom_locked = not self.zoom_locked if self.zoom_locked: self.a_lk.setIcon(QIcon(':/lock-locked.svg')) if self.a_zo.isChecked(): self.a_zo.trigger() # toggle off programmatically self.a_zo.setEnabled(False) if self.a_pa.isChecked(): self.a_pa.trigger() # toggle off programmatically self.a_pa.setEnabled(False) self.a_fv.setEnabled(False) self.a_ho.setEnabled(False) else: self.a_lk.setIcon(QIcon(':/lock-unlocked.svg')) self.a_zo.setEnabled(True) self.a_pa.setEnabled(True) self.a_fv.setEnabled(True) self.a_ho.setEnabled(True) self.sig_tx.emit({'sender':__name__, 'lock_zoom':self.zoom_locked}) #------------------------------------------------------------------------------ # ============================================================================= # def enable_plot(self, state = None): # """ # Toggle the enable button and setting and enable / disable all # buttons accordingly. # """ # if state is not None: # self.enabled = state # else: # self.enabled = not self.enabled # if self.enabled: # self.a_en.setIcon(QIcon(':/circle-x.svg')) # else: # self.a_en.setIcon(QIcon(':/circle-check.svg')) # # self.a_ho.setEnabled(self.enabled) # self.a_ba.setEnabled(self.enabled) # self.a_fw.setEnabled(self.enabled) # self.a_pa.setEnabled(self.enabled) # self.a_zo.setEnabled(self.enabled) # self.a_fv.setEnabled(self.enabled) # self.a_lk.setEnabled(self.enabled) # self.a_gr.setEnabled(self.enabled) # #self.a_rd.setEnabled(self.enabled) # self.a_sv.setEnabled(self.enabled) # self.a_cb.setEnabled(self.enabled) # self.a_op.setEnabled(self.enabled) # # self.sig_tx.emit({'sender':__name__, 'enabled':self.enabled}) # # ============================================================================= #------------------------------------------------------------------------------ def mpl2Clip(self): """ Save current figure to temporary file and copy it to the clipboard. """ try: img = QImage(self.canvas.grab()) self.cb.setImage(img) except: logger.error('Error copying figure to clipboard:\n{0}'.format(sys.exc_info()))
class FIR_DF_nmigen_UI(QWidget): """ Widget for entering word formats & quantization, also instantiates fixpoint filter class :class:`FilterFIR`. """ sig_rx = pyqtSignal(object) # incoming sig_tx = pyqtSignal(object) # outcgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self): super().__init__() self.title = ("<b>Direct-Form (DF) FIR Filter</b><br />" "Standard FIR topology.") self.img_name = "fir_df.png" self._construct_UI() # Construct an instance of the fixpoint filter using the settings from # the 'fxqc' quantizer dict # ------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the UI with widgets for coefficient format and input and output quantization """ if 'QA' not in fb.fil[0]['fxqc']: fb.fil[0]['fxqc']['QA'] = {} set_dict_defaults(fb.fil[0]['fxqc']['QA'], { 'WI': 0, 'WF': 30, 'W': 32, 'ovfl': 'wrap', 'quant': 'floor' }) self.wdg_w_coeffs = UI_W( self, fb.fil[0]['fxqc']['QCB'], wdg_name='w_coeff', label='Coeff. Format <i>B<sub>I.F </sub></i>:', tip_WI='Number of integer bits - edit in "b,a" tab', tip_WF='Number of fractional bits - edit in "b,a" tab', WI=fb.fil[0]['fxqc']['QCB']['WI'], WF=fb.fil[0]['fxqc']['QCB']['WF']) # self.wdg_q_coeffs = UI_Q(self, fb.fil[0]['fxqc']['QCB'], # cur_ov=fb.fil[0]['fxqc']['QCB']['ovfl'], # cur_q=fb.fil[0]['fxqc']['QCB']['quant']) # self.wdg_q_coeffs.sig_tx.connect(self.update_q_coeff) self.wdg_w_accu = UI_W(self, fb.fil[0]['fxqc']['QA'], label='', wdg_name='w_accu', fractional=True, combo_visible=True) self.wdg_q_accu = UI_Q(self, fb.fil[0]['fxqc']['QA'], wdg_name='q_accu', label='Accu Format <i>Q<sub>A </sub></i>:') # initial setting for accumulator cmbW = qget_cmb_box(self.wdg_w_accu.cmbW, data=False) self.wdg_w_accu.ledWF.setEnabled(cmbW == 'man') self.wdg_w_accu.ledWI.setEnabled(cmbW == 'man') # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs & EVENTFILTERS # ---------------------------------------------------------------------- self.wdg_w_coeffs.sig_tx.connect(self.update_q_coeff) self.wdg_w_accu.sig_tx.connect(self.process_sig_rx) self.wdg_q_accu.sig_tx.connect(self.process_sig_rx) # ------------------------------------------------------------------------------ layVWdg = QVBoxLayout() layVWdg.setContentsMargins(0, 0, 0, 0) layVWdg.addWidget(self.wdg_w_coeffs) # layVWdg.addWidget(self.wdg_q_coeffs) layVWdg.addWidget(self.wdg_q_accu) layVWdg.addWidget(self.wdg_w_accu) layVWdg.addStretch() self.setLayout(layVWdg) # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): logger.warning("sig_rx:\n{0}".format(pprint_log(dict_sig))) # check whether anything needs to be done locally # could also check here for 'quant', 'ovfl', 'WI', 'WF' (not needed at the moment) # if not, just emit the dict. if 'ui' in dict_sig: if dict_sig['wdg_name'] == 'w_coeff': # coefficient format updated """ Update coefficient quantization settings and coefficients. The new values are written to the fixpoint coefficient dict as `fb.fil[0]['fxqc']['QCB']` and `fb.fil[0]['fxqc']['b']`. """ fb.fil[0]['fxqc'].update(self.ui2dict()) elif dict_sig['wdg_name'] == 'w_accu': # accu format updated cmbW = qget_cmb_box(self.wdg_w_accu.cmbW, data=False) self.wdg_w_accu.ledWF.setEnabled(cmbW == 'man') self.wdg_w_accu.ledWI.setEnabled(cmbW == 'man') if cmbW in {'full', 'auto'}\ or ('ui' in dict_sig and dict_sig['ui'] in {'WF', 'WI'}): pass elif cmbW == 'man': # switched to manual, don't do anything return # Accu quantization or overflow settings have been changed elif dict_sig['wdg_name'] == 'q_accu': pass else: logger.error(f"Unknown widget name '{dict_sig['wdg_name']}' " f"in '{__name__}' !") return # - update fixpoint accu and coefficient quantization dict # - emit {'fx_sim': 'specs_changed'} fb.fil[0]['fxqc'].update(self.ui2dict()) self.emit({'fx_sim': 'specs_changed'}) else: logger.error( f"Unknown key '{dict_sig['wdg_name']}' (should be 'ui')" f"in '{__name__}' !") # ------------------------------------------------------------------------------ def update_q_coeff(self, dict_sig): """ Update coefficient quantization settings and coefficients. The new values are written to the fixpoint coefficient dict as `fb.fil[0]['fxqc']['QCB']` and `fb.fil[0]['fxqc']['b']`. """ logger.debug("update q_coeff - dict_sig:\n{0}".format( pprint_log(dict_sig))) # dict_sig.update({'ui':'C'+dict_sig['ui']}) fb.fil[0]['fxqc'].update(self.ui2dict()) logger.debug("b = {0}".format(pprint_log(fb.fil[0]['fxqc']['b']))) self.process_sig_rx(dict_sig) # ------------------------------------------------------------------------------ def update_accu_settings(self): """ Calculate number of extra integer bits needed in the accumulator (bit growth) depending on the coefficient area (sum of absolute coefficient values) for `cmbW == 'auto'` or depending on the number of coefficients for `cmbW == 'full'`. The latter works for arbitrary coefficients but requires more bits. The new values are written to the fixpoint coefficient dict `fb.fil[0]['fxqc']['QA']`. """ try: if qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "full": A_coeff = int(np.ceil(np.log2(len(fb.fil[0]['fxqc']['b'])))) elif qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "auto": A_coeff = int( np.ceil(np.log2(np.sum(np.abs(fb.fil[0]['ba'][0]))))) except Exception as e: logger.error(e) return if qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "full" or\ qget_cmb_box(self.wdg_w_accu.cmbW, data=False) == "auto": fb.fil[0]['fxqc']['QA']['WF'] = fb.fil[0]['fxqc']['QI']['WF']\ + fb.fil[0]['fxqc']['QCB']['WF'] fb.fil[0]['fxqc']['QA']['WI'] = fb.fil[0]['fxqc']['QI']['WI']\ + fb.fil[0]['fxqc']['QCB']['WI'] + A_coeff # calculate total accumulator word length and 'Q' format fb.fil[0]['fxqc']['QA']['W'] = fb.fil[0]['fxqc']['QA']['WI']\ + fb.fil[0]['fxqc']['QA']['WF'] + 1 fb.fil[0]['fxqc']['QA']['Q'] = str(fb.fil[0]['fxqc']['QA']['WI'])\ + '.' + str(fb.fil[0]['fxqc']['QA']['WF']) # update quantization settings fb.fil[0]['fxqc']['QA'].update(self.wdg_q_accu.q_dict) # update UI self.wdg_w_accu.dict2ui(fb.fil[0]['fxqc']['QA']) # ------------------------------------------------------------------------------ def dict2ui(self): """ Update all parts of the UI that need to be updated when specs have been changed outside this class, e.g. coefficients and coefficient wordlength. This also provides the initial setting for the widgets when the filter has been changed. This is called from one level above by :class:`pyfda.input_widgets.input_fixpoint_specs.Input_Fixpoint_Specs`. """ fxqc_dict = fb.fil[0]['fxqc'] if 'QA' not in fxqc_dict: fxqc_dict.update({'QA': {}}) # no accumulator settings in dict yet logger.warning("QA key missing") if 'QCB' not in fxqc_dict: fxqc_dict.update({'QCB': {}}) # no coefficient settings in dict yet logger.warning("QCB key missing") self.wdg_w_coeffs.dict2ui( fxqc_dict['QCB']) # update coefficient wordlength self.update_accu_settings() # update accumulator settings # ------------------------------------------------------------------------------ def ui2dict(self): """ Read out the quantization subwidgets and store their settings in the central fixpoint dictionary `fb.fil[0]['fxqc']` using the keys described below. Coefficients are quantized with these settings in the subdictionary under the key 'b'. Additionally, these subdictionaries are returned to the caller (``input_fixpoint_specs``) where they are used to update ``fb.fil[0]['fxqc']`` Parameters ---------- None Returns ------- fxqc_dict : dict containing the following keys and values: - 'QCB': dictionary with b coefficients quantization settings - 'QA': dictionary with accumulator quantization settings - 'b' : list of quantized b coefficients in format WI.WF """ fxqc_dict = fb.fil[0]['fxqc'] if 'QA' not in fxqc_dict: # no accumulator settings in dict yet: fxqc_dict.update({'QA': self.wdg_w_accu.q_dict}) logger.warning("Empty dict 'fxqc['QA]'!") else: fxqc_dict['QA'].update(self.wdg_w_accu.q_dict) if 'QCB' not in fxqc_dict: # no coefficient settings in dict yet fxqc_dict.update({'QCB': self.wdg_w_coeffs.q_dict}) logger.warning("Empty dict 'fxqc['QCB]'!") else: fxqc_dict['QCB'].update(self.wdg_w_coeffs.q_dict) fxqc_dict.update({ 'b': self.wdg_w_coeffs.quant_coeffs(self.wdg_w_coeffs.q_dict, fb.fil[0]['ba'][0], to_int=True) }) return fxqc_dict # ------------------------------------------------------------------------------ def init_filter(self): """ Construct an instance of the fixpoint filter object using the settings from the 'fxqc' quantizer dict """ p = fb.fil[0]['fxqc'] # parameter dictionary with coefficients etc. if not all(np.isfinite(p['b'])): logger.error("Coefficients contain non-finite values!") return if any(np.iscomplex(p['b'])): logger.error("Coefficients contain complex values!") return self.fx_filt = FIR_DF_nmigen(p) # ------------------------------------------------------------------------------ def to_hdl(self, **kwargs): """ Convert the nmigen description to Verilog """ return verilog.convert(self.fx_filt, ports=[self.fx_filt.i, self.fx_filt.o], **kwargs) # ------------------------------------------------------------------------------ def fxfilter(self, stimulus): """ Calculate the fixpoint filter response in float format for a frame of stimulus data (float). Parameters ---------- stimulus : ndarray of float One frame of stimuli data (float) scaled as WI.WF Returns ------- output : ndarray of float One frame of response data (float) scaled as WI.WF """ def process(): # convert stimulus to int by multiplying with 2 ^ WF input = np.round(stimulus * (1 << self.fx_filt.p['QI']['WF'])).astype(int) self.output = [] for i in input: yield self.fx_filt.i.eq(int(i)) yield Tick() self.output.append((yield self.fx_filt.o)) sim = Simulator(self.fx_filt) sim.add_clock(1 / 48000) sim.add_process(process) sim.run() # convert output to ndarray of float by dividing the integer response by 2 ^ WF return np.array(self.output, dtype='f') / (1 << self.fx_filt.p['QO']['WF'])
class UI_W(QWidget): """ Widget for entering integer and fractional bits. The result can be read out via the attributes `self.WI`, `self.WF` and `self.W`. The constructor accepts a dictionary for initial widget settings. The following keys are defined; default values are used for missing keys: 'wdg_name' : 'ui_w' # widget name 'label' : 'WI.WF' # widget text label 'visible' : True # Is widget visible? 'enabled' : True # Is widget enabled? 'fractional' : True # Display WF, otherwise WF=0 'lbl_sep' : '.' # label between WI and WF field 'max_led_width' : 30 # max. length of lineedit field 'WI' : 0 # number of frac. *bits* 'WI_len' : 2 # max. number of integer *digits* 'tip_WI' : 'Number of integer bits' # Mouse-over tooltip 'WF' : 15 # number of frac. *bits* 'WF_len' : 2 # max. number of frac. *digits* 'tip_WF' : 'Number of frac. bits' # Mouse-over tooltip 'lock_visible' : False # Pushbutton for locking visible 'tip_lock' : 'Lock input/output quant.'# Tooltip for lock push button 'combo_visible' : False # Enable integrated combo widget 'combo_items' : ['auto', 'full', 'man'] # Combo selection 'tip_combo' : 'Calculate Acc. width.' # tooltip for combo """ # sig_rx = pyqtSignal(object) # incoming, sig_tx = pyqtSignal(object) # outcgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent, q_dict, **kwargs): super(UI_W, self).__init__(parent) self.q_dict = q_dict # pass a dict with initial settings for construction self._construct_UI(**kwargs) self.ui2dict(s='init') # initialize the class attributes def _construct_UI(self, **kwargs): """ Construct widget from quantization dict, individual settings and the default dict below """ # default settings dict_ui = { 'wdg_name': 'ui_w', 'label': 'WI.WF', 'lbl_sep': '.', 'max_led_width': 30, 'WI': 0, 'WI_len': 2, 'tip_WI': 'Number of integer bits', 'WF': 15, 'WF_len': 2, 'tip_WF': 'Number of fractional bits', 'enabled': True, 'visible': True, 'fractional': True, 'combo_visible': False, 'combo_items': ['auto', 'full', 'man'], 'tip_combo': 'Calculate Acc. width.', 'lock_visible': False, 'tip_lock': 'Lock input/output quantization.' } #: default values if self.q_dict: dict_ui.update(self.q_dict) for k, v in kwargs.items(): if k not in dict_ui: logger.warning("Unknown key {0}".format(k)) else: dict_ui.update({k: v}) self.wdg_name = dict_ui['wdg_name'] if not dict_ui['fractional']: dict_ui['WF'] = 0 self.WI = dict_ui['WI'] self.WF = dict_ui['WF'] self.W = int(self.WI + self.WF + 1) if self.q_dict: self.q_dict.update({'WI': self.WI, 'WF': self.WF, 'W': self.W}) else: self.q_dict = {'WI': self.WI, 'WF': self.WF, 'W': self.W} lblW = QLabel(to_html(dict_ui['label'], frmt='bi'), self) self.cmbW = QComboBox(self) self.cmbW.addItems(dict_ui['combo_items']) self.cmbW.setVisible(dict_ui['combo_visible']) self.cmbW.setToolTip(dict_ui['tip_combo']) self.cmbW.setObjectName("cmbW") self.butLock = QPushButton(self) self.butLock.setCheckable(True) self.butLock.setChecked(False) self.butLock.setVisible(dict_ui['lock_visible']) self.butLock.setToolTip(dict_ui['tip_lock']) self.ledWI = QLineEdit(self) self.ledWI.setToolTip(dict_ui['tip_WI']) self.ledWI.setMaxLength(dict_ui['WI_len']) # maximum of 2 digits self.ledWI.setFixedWidth( dict_ui['max_led_width']) # width of lineedit in points self.ledWI.setObjectName("WI") lblDot = QLabel(dict_ui['lbl_sep'], self) lblDot.setVisible(dict_ui['fractional']) self.ledWF = QLineEdit(self) self.ledWF.setToolTip(dict_ui['tip_WF']) self.ledWF.setMaxLength(dict_ui['WI_len']) # maximum of 2 digits self.ledWF.setFixedWidth( dict_ui['max_led_width']) # width of lineedit in points self.ledWF.setVisible(dict_ui['fractional']) self.ledWF.setObjectName("WF") layH = QHBoxLayout() layH.addWidget(lblW) layH.addStretch() layH.addWidget(self.cmbW) layH.addWidget(self.butLock) layH.addWidget(self.ledWI) layH.addWidget(lblDot) layH.addWidget(self.ledWF) layH.setContentsMargins(0, 0, 0, 0) frmMain = QFrame(self) frmMain.setLayout(layH) layVMain = QVBoxLayout() # Widget main layout layVMain.addWidget(frmMain) layVMain.setContentsMargins(0, 5, 0, 0) # *params['wdg_margins']) self.setLayout(layVMain) # ---------------------------------------------------------------------- # INITIAL SETTINGS # ---------------------------------------------------------------------- self.ledWI.setText(qstr(dict_ui['WI'])) self.ledWF.setText(qstr(dict_ui['WF'])) frmMain.setEnabled(dict_ui['enabled']) frmMain.setVisible(dict_ui['visible']) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.ledWI.editingFinished.connect(self.ui2dict) self.ledWF.editingFinished.connect(self.ui2dict) self.butLock.clicked.connect(self.butLock_clicked) self.cmbW.currentIndexChanged.connect(self.ui2dict) # initialize button icon self.butLock_clicked(self.butLock.isChecked()) def quant_coeffs(self, q_dict: dict, coeffs: iterable, to_int: bool = False) -> list: """ Quantize the coefficients, scale and convert them to integer and return them as a list of integers This is called every time one of the coefficient subwidgets is edited or changed. Parameters: ----------- q_dict: dict Dictionary with quantizer settings for coefficients coeffs: iterable a list or ndarray of coefficients to be quantized Returns: -------- A list of integer coeffcients, quantized and scaled with the settings of the passed quantization dict """ # Create coefficient quantizer instance using the passed quantization parameters # dict from `input_widgets/input_coeffs.py` (and stored in the central # filter dict) Q_coeff = fx.Fixed(q_dict) Q_coeff.frmt = 'dec' # always use decimal format for coefficients if coeffs is None: logger.error("Coeffs empty!") # quantize floating point coefficients with the selected scale (WI.WF), # next convert array float -> array of fixp # -> list of int (scaled by 2^WF) when `to_int == True` if to_int: return list(Q_coeff.float2frmt(coeffs) * (1 << Q_coeff.WF)) else: return list(Q_coeff.fixp(coeffs)) # -------------------------------------------------------------------------- def butLock_clicked(self, clicked): """ Update the icon of the push button depending on its state """ if clicked: self.butLock.setIcon(QIcon(':/lock-locked.svg')) else: self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) q_icon_size = self.butLock.iconSize( ) # <- uncomment this for manual sizing self.butLock.setIconSize(q_icon_size) dict_sig = {'wdg_name': self.wdg_name, 'ui': 'butLock'} self.emit(dict_sig) # -------------------------------------------------------------------------- def ui2dict(self, s=None): """ Update the attributes `self.WI`, `self.WF` and `self.W` and `self.q_dict` when one of the QLineEdit widgets has been edited. Emit a signal with `{'ui':objectName of the sender}`. """ self.WI = int( safe_eval(self.ledWI.text(), self.WI, return_type="int", sign='poszero')) self.ledWI.setText(qstr(self.WI)) self.WF = int( safe_eval(self.ledWF.text(), self.WF, return_type="int", sign='poszero')) self.ledWF.setText(qstr(self.WF)) self.W = int(self.WI + self.WF + 1) self.q_dict.update({'WI': self.WI, 'WF': self.WF, 'W': self.W}) if self.sender(): obj_name = self.sender().objectName() logger.debug("sender: {0}".format(obj_name)) dict_sig = {'wdg_name': self.wdg_name, 'ui': obj_name} self.emit(dict_sig) elif s == 'init': logger.debug("called by __init__") else: logger.error("sender without name!") # -------------------------------------------------------------------------- def dict2ui(self, q_dict=None): """ Update the widgets `WI` and `WF` and the corresponding attributes from the dict passed as the argument """ if q_dict is None: q_dict = self.q_dict if 'WI' in q_dict: self.WI = safe_eval(q_dict['WI'], self.WI, return_type="int", sign='poszero') self.ledWI.setText(qstr(self.WI)) else: logger.warning("No key 'WI' in dict!") if 'WF' in q_dict: self.WF = safe_eval(q_dict['WF'], self.WF, return_type="int", sign='poszero') self.ledWF.setText(qstr(self.WF)) else: logger.warning("No key 'WF' in dict!") self.W = self.WF + self.WI + 1
class Firwin(QWidget): FRMT = 'ba' # output format(s) of filter design routines 'zpk' / 'ba' / 'sos' # currently, only 'ba' is supported for firwin routines sig_tx = pyqtSignal( object) # local signal between FFT widget and FFTWin_Selector sig_tx_local = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self): QWidget.__init__(self) self.ft = 'FIR' win_names_list = [ "Boxcar", "Rectangular", "Barthann", "Bartlett", "Blackman", "Blackmanharris", "Bohman", "Cosine", "Dolph-Chebyshev", "Flattop", "General Gaussian", "Gauss", "Hamming", "Hann", "Kaiser", "Nuttall", "Parzen", "Slepian", "Triangular", "Tukey" ] self.cur_win_name = "Kaiser" # set initial window type self.alg = "ichige" # initialize windows dict with the list above for firwin window settings self.win_dict = get_windows_dict(win_names_list=win_names_list, cur_win_name=self.cur_win_name) # get initial / last setting from dictionary, updating self.win_dict self._load_dict() # instantiate FFT window with windows dict self.fft_widget = Plot_FFT_win(self, win_dict=self.win_dict, sym=True, title="pyFDA FIR Window Viewer") # hide window initially, this is modeless i.e. a non-blocking popup window self.fft_widget.hide() c = Common() self.rt_dict = c.rt_base_iir self.rt_dict_add = { 'COM': { 'min': { 'msg': ('a', "<br /><b>Note:</b> Filter order is only a rough " "approximation and most likely far too low!") }, 'man': { 'msg': ('a', "Enter desired filter order <b><i>N</i></b> and " "<b>-6 dB</b> pass band corner " "frequency(ies) <b><i>F<sub>C</sub></i></b> .") }, }, 'LP': { 'man': {}, 'min': {} }, 'HP': { 'man': { 'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!") }, 'min': {} }, 'BS': { 'man': { 'msg': ('a', r"<br /><b>Note:</b> Order needs to be odd!") }, 'min': {} }, 'BP': { 'man': {}, 'min': {} }, } self.info = """**Windowed FIR filters** are designed by truncating the infinite impulse response of an ideal filter with a window function. The kind of used window has strong influence on ripple etc. of the resulting filter. **Design routines:** ``scipy.signal.firwin()`` """ # self.info_doc = [] is set in self._update_UI() # ------------------- end of static info for filter tree --------------- # ------------------------------------------------------------------------------ def process_sig_rx(self, dict_sig=None): """ Process local signals from / for - FFT window widget - qfft_win_select """ logger.debug("SIG_RX - vis: {0}\n{1}".format(self.isVisible(), pprint_log(dict_sig))) if dict_sig['id'] == id(self): logger.warning(f"Stopped infinite loop:\n{pprint_log(dict_sig)}") # --- signals coming from the FFT window widget or the qfft_win_select if dict_sig['class'] in {'Plot_FFT_win', 'QFFTWinSelector'}: if 'closeEvent' in dict_sig: # hide FFT window windget and return self.hide_fft_wdg() return else: if 'view_changed' in dict_sig and 'fft_win' in dict_sig[ 'view_changed']: # self._update_fft_window() # TODO: needed? # local connection to FFT window widget and qfft_win_select self.emit(dict_sig, sig_name='sig_tx_local') # global connection to upper hierachies # send notification that filter design has changed self.emit({'filt_changed': 'firwin'}) # -------------------------------------------------------------------------- def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter object, fb.filObj . """ # Combobox for selecting the algorithm to estimate minimum filter order self.cmb_firwin_alg = QComboBox(self) self.cmb_firwin_alg.setObjectName('wdg_cmb_firwin_alg') self.cmb_firwin_alg.addItems(['ichige', 'kaiser', 'herrmann']) # Minimum size, can be changed in the upper hierarchy levels using layouts: self.cmb_firwin_alg.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmb_firwin_alg.hide() self.qfft_win_select = QFFTWinSelector(self, self.win_dict) # Minimum size, can be changed in the upper hierarchy levels using layouts: # self.qfft_win_select.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.but_fft_wdg = QPushButton(self) self.but_fft_wdg.setIcon(QIcon(":/fft.svg")) but_height = self.qfft_win_select.sizeHint().height() self.but_fft_wdg.setIconSize(QSize(but_height, but_height)) self.but_fft_wdg.setFixedSize(QSize(but_height, but_height)) self.but_fft_wdg.setToolTip( '<span>Show / hide FFT widget (select window type ' ' and display its properties).</span>') self.but_fft_wdg.setCheckable(True) self.but_fft_wdg.setChecked(False) self.layHWin1 = QHBoxLayout() # self.layHWin1.addWidget(self.cmb_firwin_win) # self.layHWin1.addWidget(self.but_fft_wdg) self.layHWin1.addWidget(self.cmb_firwin_alg) self.layHWin2 = QHBoxLayout() self.layHWin2.addWidget(self.but_fft_wdg) self.layHWin2.addWidget(self.qfft_win_select) self.layVWin = QVBoxLayout() self.layVWin.addLayout(self.layHWin1) self.layVWin.addLayout(self.layHWin2) self.layVWin.setContentsMargins(0, 0, 0, 0) # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits) self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layVWin) # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- # connect FFT widget to qfft_selector and vice versa and to signals upstream: self.fft_widget.sig_tx.connect(self.process_sig_rx) self.qfft_win_select.sig_tx.connect(self.process_sig_rx) # connect process_sig_rx output to both FFT widgets self.sig_tx_local.connect(self.fft_widget.sig_rx) self.sig_tx_local.connect(self.qfft_win_select.sig_rx) # ---------------------------------------------------------------------- # SIGNALS & SLOTs # ---------------------------------------------------------------------- self.cmb_firwin_alg.currentIndexChanged.connect( self._update_fft_window) self.but_fft_wdg.clicked.connect(self.toggle_fft_wdg) # ---------------------------------------------------------------------- # ============================================================================== def _update_fft_window(self): """ Update window type for FirWin - unneeded at the moment """ self.alg = str(self.cmb_firwin_alg.currentText()) self.emit({'filt_changed': 'firwin'}) # -------------------------------------------------------------------------- def _load_dict(self): """ Reload window selection and parameters from filter dictionary and set UI elements accordingly. load_dict() is called upon initialization and when the filter is loaded from disk. """ self.N = fb.fil[0]['N'] # alg_idx = 0 if 'wdg_fil' in fb.fil[0] and 'firwin' in fb.fil[0]['wdg_fil']\ and type(fb.fil[0]['wdg_fil']['firwin']) is dict: self.win_dict = fb.fil[0]['wdg_fil']['firwin'] self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_local') # -------------------------------------------------------------------------- def _store_dict(self): """ Store window and parameter settings using `self.win_dict` in filter dictionary. """ if 'wdg_fil' not in fb.fil[0]: fb.fil[0].update({'wdg_fil': {}}) fb.fil[0]['wdg_fil'].update({'firwin': self.win_dict}) # -------------------------------------------------------------------------- def _get_params(self, fil_dict): """ Translate parameters from the passed dictionary to instance parameters, scaling / transforming them if needed. """ self.N = fil_dict['N'] self.F_PB = fil_dict['F_PB'] self.F_SB = fil_dict['F_SB'] self.F_PB2 = fil_dict['F_PB2'] self.F_SB2 = fil_dict['F_SB2'] self.F_C = fil_dict['F_C'] self.F_C2 = fil_dict['F_C2'] # firwin amplitude specs are linear (not in dBs) self.A_PB = fil_dict['A_PB'] self.A_PB2 = fil_dict['A_PB2'] self.A_SB = fil_dict['A_SB'] self.A_SB2 = fil_dict['A_SB2'] # self.alg = 'ichige' # algorithm for determining the minimum order # self.alg = self.cmb_firwin_alg.currentText() def _test_N(self): """ Warn the user if the calculated order is too high for a reasonable filter design. """ if self.N > 1000: return qfilter_warning(self, self.N, "FirWin") else: return True def _save(self, fil_dict, arg): """ Convert between poles / zeros / gain, filter coefficients (polynomes) and second-order sections and store all available formats in the passed dictionary 'fil_dict'. """ fil_save(fil_dict, arg, self.FRMT, __name__) try: # has the order been calculated by a "min" filter design? fil_dict['N'] = self.N # yes, update filterbroker except AttributeError: pass self._store_dict() # ------------------------------------------------------------------------------ def firwin(self, numtaps, cutoff, window=None, pass_zero=True, scale=True, nyq=1.0, fs=None): """ FIR filter design using the window method. This is more or less the same as `scipy.signal.firwin` with the exception that an ndarray with the window values can be passed as an alternative to the window name. The parameters "width" (specifying a Kaiser window) and "fs" have been omitted, they are not needed here. This function computes the coefficients of a finite impulse response filter. The filter will have linear phase; it will be Type I if `numtaps` is odd and Type II if `numtaps` is even. Type II filters always have zero response at the Nyquist rate, so a ValueError exception is raised if firwin is called with `numtaps` even and having a passband whose right end is at the Nyquist rate. Parameters ---------- numtaps : int Length of the filter (number of coefficients, i.e. the filter order + 1). `numtaps` must be even if a passband includes the Nyquist frequency. cutoff : float or 1D array_like Cutoff frequency of filter (expressed in the same units as `nyq`) OR an array of cutoff frequencies (that is, band edges). In the latter case, the frequencies in `cutoff` should be positive and monotonically increasing between 0 and `nyq`. The values 0 and `nyq` must not be included in `cutoff`. window : ndarray or string string: use the window with the passed name from scipy.signal.windows ndarray: The window values - this is an addition to the original firwin routine. pass_zero : bool, optional If True, the gain at the frequency 0 (i.e. the "DC gain") is 1. Otherwise the DC gain is 0. scale : bool, optional Set to True to scale the coefficients so that the frequency response is exactly unity at a certain frequency. That frequency is either: - 0 (DC) if the first passband starts at 0 (i.e. pass_zero is True) - `nyq` (the Nyquist rate) if the first passband ends at `nyq` (i.e the filter is a single band highpass filter); center of first passband otherwise nyq : float, optional Nyquist frequency. Each frequency in `cutoff` must be between 0 and `nyq`. Returns ------- h : (numtaps,) ndarray Coefficients of length `numtaps` FIR filter. Raises ------ ValueError If any value in `cutoff` is less than or equal to 0 or greater than or equal to `nyq`, if the values in `cutoff` are not strictly monotonically increasing, or if `numtaps` is even but a passband includes the Nyquist frequency. See also -------- scipy.firwin """ cutoff = np.atleast_1d(cutoff) / float(nyq) # Check for invalid input. if cutoff.ndim > 1: raise ValueError("The cutoff argument must be at most " "one-dimensional.") if cutoff.size == 0: raise ValueError("At least one cutoff frequency must be given.") if cutoff.min() <= 0 or cutoff.max() >= 1: raise ValueError( "Invalid cutoff frequency {0}: frequencies must be " "greater than 0 and less than nyq.".format(cutoff)) if np.any(np.diff(cutoff) <= 0): raise ValueError("Invalid cutoff frequencies: the frequencies " "must be strictly increasing.") pass_nyquist = bool(cutoff.size & 1) ^ pass_zero if pass_nyquist and numtaps % 2 == 0: raise ValueError( "A filter with an even number of coefficients must " "have zero response at the Nyquist rate.") # Insert 0 and/or 1 at the ends of cutoff so that the length of cutoff # is even, and each pair in cutoff corresponds to passband. cutoff = np.hstack(([0.0] * pass_zero, cutoff, [1.0] * pass_nyquist)) # `bands` is a 2D array; each row gives the left and right edges of # a passband. bands = cutoff.reshape(-1, 2) # Build up the coefficients. alpha = 0.5 * (numtaps - 1) m = np.arange(0, numtaps) - alpha h = 0 for left, right in bands: h += right * sinc(right * m) h -= left * sinc(left * m) if type(window) == str: # Get and apply the window function. # from scipy.signal.signaltools import get_window win = signaltools.get_window(window, numtaps, fftbins=False) elif type(window) == np.ndarray: win = window else: logger.error( "The 'window' was neither a string nor a numpy array, " "it could not be evaluated.") return None # apply the window function. h *= win # Now handle scaling if desired. if scale: # Get the first passband. left, right = bands[0] if left == 0: scale_frequency = 0.0 elif right == 1: scale_frequency = 1.0 else: scale_frequency = 0.5 * (left + right) c = np.cos(np.pi * m * scale_frequency) s = np.sum(h * c) h /= s return h def _firwin_ord(self, F, W, A, alg): # http://www.mikroe.com/chapters/view/72/chapter-2-fir-filters/ delta_f = abs(F[1] - F[0]) * 2 # referred to f_Ny # delta_A = np.sqrt(A[0] * A[1]) if "Kaiser" in self.win_dict and self.win_dict[ 'cur_win_name'] == "Kaiser": N, beta = sig.kaiserord(20 * np.log10(np.abs(fb.fil[0]['A_SB'])), delta_f) # logger.warning(f"N={N}, beta={beta}, A_SB={fb.fil[0]['A_SB']}") self.win_dict["Kaiser"]["par"][0]["val"] = beta self.qfft_win_select.led_win_par_0.setText(str(beta)) self.qfft_win_select.ui2dict_params( ) # pass changed parameter to other widgets else: N = remezord(F, W, A, fs=1, alg=alg)[0] self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_local') return N def LPmin(self, fil_dict): self._get_params(fil_dict) self.N = self._firwin_ord([self.F_PB, self.F_SB], [1, 0], [self.A_PB, self.A_SB], alg=self.alg) if not self._test_N(): return -1 fil_dict['F_C'] = (self.F_SB + self.F_PB) / 2 # average calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], nyq=0.5, window=self.qfft_win_select.get_window(self.N, sym=True))) def LPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 logger.warning(self.win_dict["cur_win_name"]) self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], nyq=0.5, window=self.qfft_win_select.get_window(self.N, sym=True))) def HPmin(self, fil_dict): self._get_params(fil_dict) N = self._firwin_ord([self.F_SB, self.F_PB], [0, 1], [self.A_SB, self.A_PB], alg=self.alg) self.N = round_odd(N) # enforce odd order if not self._test_N(): return -1 fil_dict['F_C'] = (self.F_SB + self.F_PB) / 2 # average calculated F_PB and F_SB self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], pass_zero=False, nyq=0.5, window=self.qfft_win_select.get_window(self.N, sym=True))) def HPman(self, fil_dict): self._get_params(fil_dict) self.N = round_odd(self.N) # enforce odd order if not self._test_N(): return -1 self._save( fil_dict, self.firwin(self.N, fil_dict['F_C'], pass_zero=False, nyq=0.5, window=self.qfft_win_select.get_window(self.N, sym=True))) # For BP and BS, F_PB and F_SB have two elements each def BPmin(self, fil_dict): self._get_params(fil_dict) self.N = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2], [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2], fs=1, alg=self.alg)[0] if not self._test_N(): return -1 fil_dict['F_C'] = (self.F_SB + self.F_PB) / 2 # average calculated F_PB and F_SB fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2) / 2 self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], nyq=0.5, pass_zero=False, window=self.qfft_win_select.get_window(self.N, sym=True))) def BPman(self, fil_dict): self._get_params(fil_dict) if not self._test_N(): return -1 self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], nyq=0.5, pass_zero=False, window=self.qfft_win_select.get_window(self.N, sym=True))) def BSmin(self, fil_dict): self._get_params(fil_dict) N = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2], [1, 0, 1], [self.A_PB, self.A_SB, self.A_PB2], fs=1, alg=self.alg)[0] self.N = round_odd(N) # enforce odd order if not self._test_N(): return -1 fil_dict['F_C'] = (self.F_SB + self.F_PB) / 2 # average calculated F_PB and F_SB fil_dict['F_C2'] = (self.F_SB2 + self.F_PB2) / 2 self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.qfft_win_select.get_window(self.N, sym=True), pass_zero=True, nyq=0.5)) def BSman(self, fil_dict): self._get_params(fil_dict) self.N = round_odd(self.N) # enforce odd order if not self._test_N(): return -1 self._save( fil_dict, self.firwin(self.N, [fil_dict['F_C'], fil_dict['F_C2']], window=self.qfft_win_select.get_window(self.N, sym=True), pass_zero=True, nyq=0.5)) # ------------------------------------------------------------------------------ def toggle_fft_wdg(self): """ Show / hide FFT widget depending on the state of the corresponding button When widget is shown, trigger an update of the window function. """ if self.but_fft_wdg.isChecked(): self.fft_widget.show() self.emit({'view_changed': 'fft_win_type'}, sig_name='sig_tx_local') else: self.fft_widget.hide() # -------------------------------------------------------------------------- def hide_fft_wdg(self): """ The closeEvent caused by clicking the "x" in the FFT widget is caught there and routed here to only hide the window """ self.but_fft_wdg.setChecked(False) self.fft_widget.hide()
class Input_Specs(QWidget): """ Build widget for entering all filter specs """ # class variables (shared between instances if more than one exists) sig_rx_local = pyqtSignal( object) # incoming from subwidgets -> process_sig_rx_local sig_rx = pyqtSignal(object) # incoming from subwidgets -> process_sig_rx sig_tx = pyqtSignal(object) # from process_sig_rx: propagate local signals from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(Input_Specs, self).__init__(parent) self.tab_label = "Specs" self.tool_tip = "Enter and view filter specifications." self._construct_UI() def process_sig_rx_local(self, dict_sig=None): """ Flag signals coming in from local subwidgets with `propagate=True` before proceeding with processing in `process_sig_rx`. """ self.process_sig_rx(dict_sig, propagate=True) def process_sig_rx(self, dict_sig=None, propagate=False): """ Process signals coming in via subwidgets and sig_rx All signals terminate here unless the flag `propagate=True`. The sender name of signals coming in from local subwidgets is changed to its parent widget (`input_specs`) to prevent infinite loops. """ # logger.debug(f"SIG_RX: {pprint_log(dict_sig)}") if dict_sig['id'] == id(self): # logger.warning(f"Stopped infinite loop:\n\tPropagate = {propagate}\ # \n{pprint_log(dict_sig)}") return elif 'view_changed' in dict_sig: self.f_specs.load_dict() self.t_specs.load_dict() elif 'specs_changed' in dict_sig: self.f_specs.sort_dict_freqs() self.t_specs.f_specs.sort_dict_freqs() self.color_design_button("changed") elif 'filt_changed' in dict_sig: # Changing the filter design requires updating UI because number or # kind of input fields changes -> call update_UI self.update_UI(dict_sig) elif 'data_changed' in dict_sig: if dict_sig['data_changed'] == 'filter_loaded': """ Called when a new filter has been LOADED: Pass new filter data from the global filter dict by specifically calling SelectFilter.load_dict() """ self.sel_fil.load_dict() # update select_filter widget # Pass new filter data from the global filter dict & set button = "ok" self.load_dict() if propagate: # local signals are propagated with the name of this widget, # global signals terminate here dict_sig.update({'class': self.__class__.__name__}) self.emit(dict_sig) def _construct_UI(self): """ Construct User Interface from all input subwidgets """ self.butLoadFilt = QPushButton("LOAD FILTER", self) self.butLoadFilt.setToolTip("Load filter from disk") self.butSaveFilt = QPushButton("SAVE FILTER", self) self.butSaveFilt.setToolTip("Save filter todisk") layHButtons1 = QHBoxLayout() layHButtons1.addWidget(self.butLoadFilt) # <Load Filter> button layHButtons1.addWidget(self.butSaveFilt) # <Save Filter> button layHButtons1.setContentsMargins(*params['wdg_margins_spc']) self.butDesignFilt = QPushButton("DESIGN FILTER", self) self.butDesignFilt.setToolTip("Design filter with chosen specs") self.butQuit = QPushButton("Quit", self) self.butQuit.setToolTip("Exit pyfda tool") layHButtons2 = QHBoxLayout() layHButtons2.addWidget(self.butDesignFilt) # <Design Filter> button layHButtons2.addWidget(self.butQuit) # <Quit> button layHButtons2.setContentsMargins(*params['wdg_margins']) # Subwidget for selecting filter with response type rt (LP, ...), # filter type ft (IIR, ...) and filter class fc (cheby1, ...) self.sel_fil = select_filter.SelectFilter(self) self.sel_fil.setObjectName("select_filter") self.sel_fil.sig_tx.connect(self.sig_rx_local) # Subwidget for selecting the frequency unit and range self.f_units = freq_units.FreqUnits(self) self.f_units.setObjectName("freq_units") self.f_units.sig_tx.connect(self.sig_rx_local) # Changing the frequency unit requires re-display of frequency specs # but it does not influence the actual specs (no specsChanged ) # Activating the "Sort" button emits 'view_changed'?specs_changed'?, requiring # sorting and storing the frequency entries # Changing filter parameters / specs requires reloading of parameters # in other hierarchy levels, e.g. in the plot tabs # Subwidget for Frequency Specs self.f_specs = freq_specs.FreqSpecs(self) self.f_specs.setObjectName("freq_specs") self.f_specs.sig_tx.connect(self.sig_rx_local) self.sig_tx.connect(self.f_specs.sig_rx) # Subwidget for Amplitude Specs self.a_specs = amplitude_specs.AmplitudeSpecs(self) self.a_specs.setObjectName("amplitude_specs") self.a_specs.sig_tx.connect(self.sig_rx_local) # Subwidget for Weight Specs self.w_specs = weight_specs.WeightSpecs(self) self.w_specs.setObjectName("weight_specs") self.w_specs.sig_tx.connect(self.sig_rx_local) # Subwidget for target specs (frequency and amplitude) self.t_specs = target_specs.TargetSpecs(self, title="Target Specifications") self.t_specs.setObjectName("target_specs") self.t_specs.sig_tx.connect(self.sig_rx_local) self.sig_tx.connect(self.t_specs.sig_rx) # Subwidget for displaying infos on the design method self.lblMsg = QLabel(self) self.lblMsg.setWordWrap(True) layVMsg = QVBoxLayout() layVMsg.addWidget(self.lblMsg) self.frmMsg = QFrame(self) self.frmMsg.setLayout(layVMsg) layVFrm = QVBoxLayout() layVFrm.addWidget(self.frmMsg) layVFrm.setContentsMargins(*params['wdg_margins']) # ---------------------------------------------------------------------- # LAYOUT for input specifications and buttons # ---------------------------------------------------------------------- layVMain = QVBoxLayout(self) layVMain.addLayout(layHButtons1) # <Load> & <Save> buttons layVMain.addWidget(self.sel_fil) # Design method (IIR - ellip, ...) layVMain.addLayout(layHButtons2) # <Design> & <Quit> buttons layVMain.addWidget(self.f_units) # Frequency units layVMain.addWidget(self.t_specs) # Target specs layVMain.addWidget(self.f_specs) # Freq. specifications layVMain.addWidget(self.a_specs) # Amplitude specs layVMain.addWidget(self.w_specs) # Weight specs layVMain.addLayout(layVFrm) # Text message layVMain.addStretch() layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) # main layout of widget # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) # ---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs # ---------------------------------------------------------------------- self.sig_rx_local.connect(self.process_sig_rx_local) self.butLoadFilt.clicked.connect(lambda: load_filter(self)) self.butSaveFilt.clicked.connect(lambda: save_filter(self)) self.butDesignFilt.clicked.connect(self.start_design_filt) self.butQuit.clicked.connect(self.quit_program) # emit 'quit_program' # ---------------------------------------------------------------------- self.update_UI() # first time initialization self.start_design_filt() # design first filter using default values # ------------------------------------------------------------------------------ def update_UI(self, dict_sig={}): """ update_UI is called every time the filter design method or order (min / man) has been changed as this usually requires a different set of frequency and amplitude specs. At this time, the actual filter object instance has been created from the name of the design method (e.g. 'cheby1') in select_filter.py. Its handle has been stored in fb.fil_inst. fb.fil[0] (currently selected filter) is read, then general information for the selected filter type and order (min/man) is gathered from the filter tree [fb.fil_tree], i.e. which parameters are needed, which widgets are visible and which message shall be displayed. Then, the UIs of all subwidgets are updated using their "update_UI" method. """ rt = fb.fil[0]['rt'] # e.g. 'LP' ft = fb.fil[0]['ft'] # e.g. 'FIR' fc = fb.fil[0]['fc'] # e.g. 'equiripple' fo = fb.fil[0]['fo'] # e.g. 'man' # the keys of the all_widgets dict are the names of the subwidgets, # the values are a tuple with the corresponding parameters all_widgets = fb.fil_tree[rt][ft][fc][fo] # logger.debug("rt: {0} - ft: {1} - fc: {2} - fo: {3}".format(rt, ft, fc, fo)) # logger.debug("fb.fil_tree[rt][ft][fc][fo]:\n{0}".format(fb.fil_tree[rt][ft][fc][fo])) # update filter order subwidget, called by select_filter: # self.sel_fil.load_filter_order() # TARGET SPECS: is widget in the dict and is it visible (marker != 'i')? if ('tspecs' in all_widgets and len(all_widgets['tspecs']) > 1 and all_widgets['tspecs'][0] != 'i'): self.t_specs.setVisible(True) # disable all subwidgets with marker 'd': self.t_specs.setEnabled(all_widgets['tspecs'][0] != 'd') self.t_specs.update_UI(new_labels=all_widgets['tspecs'][1]) else: self.t_specs.hide() # FREQUENCY SPECS if ('fspecs' in all_widgets and len(all_widgets['fspecs']) > 1 and all_widgets['fspecs'][0] != 'i'): self.f_specs.setVisible(True) self.f_specs.setEnabled(all_widgets['fspecs'][0] != 'd') self.f_specs.update_UI(new_labels=all_widgets['fspecs']) else: self.f_specs.hide() # AMPLITUDE SPECS if ('aspecs' in all_widgets and len(all_widgets['aspecs']) > 1 and all_widgets['aspecs'][0] != 'i'): self.a_specs.setVisible(True) self.a_specs.setEnabled(all_widgets['aspecs'][0] != 'd') self.a_specs.update_UI(new_labels=all_widgets['aspecs']) else: self.a_specs.hide() # WEIGHT SPECS if ('wspecs' in all_widgets and len(all_widgets['wspecs']) > 1 and all_widgets['wspecs'][0] != 'i'): self.w_specs.setVisible(True) self.w_specs.setEnabled(all_widgets['wspecs'][0] != 'd') self.w_specs.update_UI(new_labels=all_widgets['wspecs']) else: self.w_specs.hide() # MESSAGE PANE if ('msg' in all_widgets and len(all_widgets['msg']) > 1 and all_widgets['msg'][0] != 'i'): self.frmMsg.setVisible(True) self.frmMsg.setEnabled(all_widgets['msg'][0] != 'd') self.lblMsg.setText(all_widgets['msg'][1:][0]) else: self.frmMsg.hide() # Update state of "DESIGN FILTER" button # It is disabled for "Manual_IIR" and "Manual_FIR" filter classes self.color_design_button("changed") # ------------------------------------------------------------------------------ def load_dict(self): """ Reload all specs/parameters entries from global dict fb.fil[0], using the "load_dict" methods of the individual classes """ self.sel_fil.load_dict() # select filter widget self.f_units.load_dict() # frequency units widget self.f_specs.load_dict() # frequency specification widget self.a_specs.load_dict() # magnitude specs with unit self.w_specs.load_dict() # weight specification self.t_specs.load_dict() # target specs self.color_design_button("ok") # ------------------------------------------------------------------------------ def start_design_filt(self): """ Start the actual filter design process: - store the entries of all input widgets in the global filter dict. - call the design method, passing the whole dictionary as the argument: let the design method pick the needed specs - update the input widgets in case weights, corner frequencies etc. have been changed by the filter design method - the plots are updated via signal-slot connection """ try: logger.info( "Start filter design using method\n\t'{0}.{1}{2}'".format( str(fb.fil[0]['fc']), str(fb.fil[0]['rt']), str(fb.fil[0]['fo']))) # ---------------------------------------------------------------------- # A globally accessible instance fb.fil_inst of selected filter class fc # has been instantiated in InputFilter.set_design_method, now # call the method specified in the filter dict fil[0]. # The name of the instance method is constructed from the response # type (e.g. 'LP') and the filter order (e.g. 'man'), giving e.g. 'LPman'. # The filter is designed by passing the specs in fil[0] to the method, # resulting in e.g. cheby1.LPman(fb.fil[0]) and writing back coefficients, # P/Z etc. back to fil[0]. err = ff.fil_factory.call_fil_method( fb.fil[0]['rt'] + fb.fil[0]['fo'], fb.fil[0]) # this is the same as e.g. # from pyfda.filter_design import ellip # inst = ellip.ellip() # inst.LPmin(fb.fil[0]) # ----------------------------------------------------------------------- if err > 0: self.color_design_button("error") elif err == -1: # filter design cancelled by user return else: # Update filter order. weights and freq display in case they # have been changed by the design algorithm self.sel_fil.load_filter_order() self.w_specs.load_dict() self.f_specs.load_dict() self.color_design_button("ok") self.emit({'data_changed': 'filter_designed'}) logger.info('Designed filter with order = {0}'.format( str(fb.fil[0]['N']))) # ============================================================================= # logger.debug("Results:\n" # "F_PB = %s, F_SB = %s " # "Filter order N = %s\n" # "NDim fil[0]['ba'] = %s\n\n" # "b,a = %s\n\n" # "zpk = %s\n", # str(fb.fil[0]['F_PB']), str(fb.fil[0]['F_SB']), str(fb.fil[0]['N']), # str(np.ndim(fb.fil[0]['ba'])), pformat(fb.fil[0]['ba']), # pformat(fb.fil[0]['zpk'])) # # ============================================================================= except Exception as e: if ('__doc__' in str(e)): logger.warning("Filter design:\n %s\n %s\n", e.__doc__, e) else: logger.warning("{0}".format(e)) self.color_design_button("error") def color_design_button(self, state): man = "manual" in fb.fil[0]['fc'].lower() self.butDesignFilt.setDisabled(man) if man: state = 'ok' fb.design_filt_state = state qstyle_widget(self.butDesignFilt, state) # ------------------------------------------------------------------------------ def quit_program(self): """ When <QUIT> button is pressed, send 'quit_program' """ self.emit({'quit_program': ''})
class TargetSpecs(QWidget): """ Build and update widget for entering the target specifications (frequencies and amplitudes) like F_SB, F_PB, A_SB, etc. """ # class variables (shared between instances if more than one exists) sig_rx = pyqtSignal(object) # incoming sig_tx = pyqtSignal(object) # outgoing # from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None, title="Target Specs"): super(TargetSpecs, self).__init__(parent) self.title = title self._construct_UI() # ============================================================================= # #------------------------------------------------------------------------------ # def process_sig_rx(self, dict_sig=None): # """ # Process signals coming in via subwidgets and sig_rx # """ # logger.warning("Processing {0}: {1}".format(type(dict_sig).__name__, dict_sig)) # if dict_sig['id'] == id(self): # logger.warning("Stopped infinite loop:\n{0}".format(pprint_log(dict_sig))) # return # elif 'view_changed' in dict_sig and dict_sig['view_changed'] == 'f_S': # # update target frequencies with new f_S # self.f_specs.recalc_freqs() # # ============================================================================= def _construct_UI(self): """ Construct user interface """ # subwidget for Frequency Specs self.f_specs = freq_specs.FreqSpecs(self, title="Frequency") # subwidget for Amplitude Specs self.a_specs = amplitude_specs.AmplitudeSpecs(self, title="Amplitude") self.a_specs.setVisible(True) """ LAYOUT """ bfont = QFont() bfont.setBold(True) lblTitle = QLabel(self) # field for widget title lblTitle.setText(self.title) lblTitle.setFont(bfont) # lblTitle.setContentsMargins(2,2,2,2) layHTitle = QHBoxLayout() layHTitle.addWidget(lblTitle) layHTitle.setAlignment(Qt.AlignHCenter) layHSpecs = QHBoxLayout() layHSpecs.setAlignment(Qt.AlignTop) layHSpecs.addWidget(self.f_specs) # frequency specs layHSpecs.addWidget(self.a_specs) # ampltitude specs layVSpecs = QVBoxLayout() layVSpecs.addLayout(layHTitle) layVSpecs.addLayout(layHSpecs) layVSpecs.setContentsMargins(0, 6, 0, 0) # (left, top, right, bottom) # This is the top level widget, encompassing the other widgets frmMain = QFrame(self) frmMain.setLayout(layVSpecs) self.layVMain = QVBoxLayout() # Widget main layout self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) # ---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs # ---------------------------------------------------------------------- # connect f_specs and a_specs subwidget to signalling self.f_specs.sig_tx.connect(self.sig_tx) # pass signal upwards self.sig_rx.connect(self.f_specs.sig_rx) # pass on received signals self.a_specs.sig_tx.connect(self.sig_tx) # pass signal upwards self.update_UI() # first time initialization # ------------------------------------------------------------------------------ def update_UI(self, new_labels=()): """ Called when a new filter design algorithm has been selected - Pass new frequency and amplitude labels to the amplitude and frequency spec widgets. The first element of the 'amp' and the 'freq' tuple is the state with 'u' for 'unused' and 'd' for disabled - The `filt_changed` signal is emitted already by `select_filter.py` """ if ('frq' in new_labels and len(new_labels['frq']) > 1 and new_labels['frq'][0] != 'i'): self.f_specs.show() self.f_specs.setEnabled(new_labels['frq'][0] != 'd') self.f_specs.update_UI(new_labels=new_labels['frq']) else: self.f_specs.hide() if ('amp' in new_labels and len(new_labels['amp']) > 1 and new_labels['amp'][0] != 'i'): self.a_specs.show() self.a_specs.setEnabled(new_labels['amp'][0] != 'd') self.a_specs.update_UI(new_labels=new_labels['amp']) else: self.a_specs.hide() # self.emit({'changed_specs':'target'}) # ------------------------------------------------------------------------------ def load_dict(self): """ Update entries from global dict fb.fil[0] parameters, using the "load_dict" methods of the classes """ self.a_specs.load_dict() # magnitude specs with unit self.f_specs.load_dict() # weight specification
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 FreqUnits(QWidget): """ Build and update widget for entering the frequency units The following key-value pairs of the `fb.fil[0]` dict are modified: - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency values for labelling the frequency axis - `'f_S'` : The sampling frequency for referring frequency values to as a float - `'f_max'` : maximum frequency for scaling frequency axis - `'plt_fUnit'`: frequency unit as string - `'plt_tUnit'`: time unit as string - `'plt_fLabel'`: label for frequency axis - `'plt_tLabel'`: label for time axis """ # class variables (shared between instances if more than one exists) sig_tx = pyqtSignal(object) # outgoing def __init__(self, parent, title="Frequency Units"): super(FreqUnits, self).__init__(parent) self.title = title self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() def _construct_UI(self): """ Construct the User Interface """ self.layVMain = QVBoxLayout() # Widget main layout f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz'] self.t_units = ['', '', '', 's', 'ms', r'$\mu$s', 'ns'] bfont = QFont() bfont.setBold(True) self.lblUnits = QLabel(self) self.lblUnits.setText("Freq. Unit:") self.lblUnits.setFont(bfont) self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency self.ledF_S = QLineEdit() self.ledF_S.setText(str(fb.fil[0]["f_S"])) self.ledF_S.setObjectName("f_S") self.ledF_S.installEventFilter(self) # filter events self.lblF_S = QLabel(self) self.lblF_S.setText(to_html("f_S", frmt='bi')) self.cmbUnits = QComboBox(self) self.cmbUnits.setObjectName("cmbUnits") self.cmbUnits.addItems(f_units) self.cmbUnits.setToolTip( 'Select whether frequencies are specified w.r.t. \n' 'the sampling frequency "f_S", to the Nyquist frequency \n' 'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S ' 'but plots graphs over the frequency index k.') self.cmbUnits.setCurrentIndex(1) # self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))# # self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole) fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")] self.cmbFRange = QComboBox(self) self.cmbFRange.setObjectName("cmbFRange") for f in fRanges: self.cmbFRange.addItem(f[0], f[1]) self.cmbFRange.setToolTip("Select frequency range (whole or half).") self.cmbFRange.setCurrentIndex(0) # Combobox resizes with longest entry self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butSort = QToolButton(self) self.butSort.setText("Sort") self.butSort.setCheckable(True) self.butSort.setChecked(True) self.butSort.setToolTip( "Sort frequencies in ascending order when pushed.") self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}") self.layHUnits = QHBoxLayout() self.layHUnits.addWidget(self.cmbUnits) self.layHUnits.addWidget(self.cmbFRange) self.layHUnits.addWidget(self.butSort) # Create a gridLayout consisting of QLabel and QLineEdit fields # for setting f_S, the units and the actual frequency specs: self.layGSpecWdg = QGridLayout() # sublayout for spec fields self.layGSpecWdg.addWidget(self.lblF_S, 1, 0) self.layGSpecWdg.addWidget(self.ledF_S, 1, 1) self.layGSpecWdg.addWidget(self.lblUnits, 0, 0) self.layGSpecWdg.addLayout(self.layHUnits, 0, 1) frmMain = QFrame(self) frmMain.setLayout(self.layGSpecWdg) self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnits.currentIndexChanged.connect(self.update_UI) self.cmbFRange.currentIndexChanged.connect(self._freq_range) self.butSort.clicked.connect(self._store_sort_flag) #---------------------------------------------------------------------- self.update_UI() # first-time initialization #------------------------------------------------------------- def update_UI(self): """ Transform the displayed frequency spec input fields according to the units setting. 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! Signals are blocked before changing the value for f_S programmatically update_UI is called - during init - when the unit combobox is changed Finally, store freqSpecsRange and emit 'view_changed' signal via _freq_range """ idx = self.cmbUnits.currentIndex() # read index of units combobox f_unit = str(self.cmbUnits.currentText()) # and the label self.ledF_S.setVisible(f_unit not in {"f_S", "f_Ny", "k"}) # only vis. when self.lblF_S.setVisible(f_unit not in {"f_S", "f_Ny", "k"}) # not normalized f_S_scale = 1 # default setting for f_S scale if f_unit in {"f_S", "f_Ny", "k"}: # normalized frequency self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency if f_unit == "f_S": # normalized to f_S fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1. f_label = r"$F = f\, /\, f_S = \Omega \, /\, 2 \mathrm{\pi} \; \rightarrow$" elif f_unit == "f_Ny": # idx == 1: normalized to f_nyq = f_S / 2 fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2. f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$" else: fb.fil[0]['f_S'] = 1 fb.fil[0]['f_max'] = params['N_FFT'] f_label = r"$k \; \rightarrow$" t_label = r"$n \; \rightarrow$" self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) else: # Hz, kHz, ... if fb.fil[0]['freq_specs_unit'] in {"f_S", "f_Ny", "k"}: # previous setting fb.fil[0]['f_S'] = fb.fil[0][ 'f_max'] = self.fs_old # restore prev. sampling frequency self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) if f_unit == "Hz": f_S_scale = 1. elif f_unit == "kHz": f_S_scale = 1.e3 elif f_unit == "MHz": f_S_scale = 1.e6 elif f_unit == "GHz": f_S_scale = 1.e9 else: logger.warning("Unknown frequency unit {0}".format(f_unit)) f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$" t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$" if f_unit == "k": plt_f_unit = "f_S" else: plt_f_unit = f_unit fb.fil[0].update({'f_S_scale': f_S_scale}) # scale factor for f_S fb.fil[0].update({'freq_specs_unit': f_unit}) # frequency unit fb.fil[0].update({"plt_fLabel": f_label}) # label for freq. axis fb.fil[0].update({"plt_tLabel": t_label}) # label for time axis fb.fil[0].update({"plt_fUnit": plt_f_unit}) # frequency unit as string fb.fil[0].update({"plt_tUnit": self.t_units[idx]}) # time unit as string self._freq_range( ) # update f_lim setting and emit sigUnitChanged signal #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value with full precision (only if `spec_edited`== True) and display the stored value in selected format. Emit 'view_changed':'f_S' """ def _store_entry(): """ Update filter dictionary, set line edit entry with reduced precision again. """ if self.spec_edited: fb.fil[0].update({ 'f_S': safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos') }) # TODO: ?! self._freq_range(emit_sig_range=False) # update plotting range self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_S'}) self.spec_edited = False # reset flag, changed entry has been saved if source.objectName() == 'f_S': if event.type() == QEvent.FocusIn: self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: _store_entry() elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.FocusOut: _store_entry() source.setText(params['FMT'].format( fb.fil[0]['f_S'])) # reduced precision # Call base class method to continue normal event processing: return super(FreqUnits, self).eventFilter(source, event) #------------------------------------------------------------- def _freq_range(self, emit_sig_range=True): """ Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S or for double-sided spectrum between -f_S/2 and f_S/2 and emit 'view_changed':'f_range'. """ rangeType = qget_cmb_box(self.cmbFRange) fb.fil[0].update({'freqSpecsRangeType': rangeType}) f_max = fb.fil[0]["f_max"] if rangeType == 'whole': f_lim = [0, f_max] elif rangeType == 'sym': f_lim = [-f_max / 2., f_max / 2.] else: f_lim = [0, f_max / 2.] fb.fil[0]['freqSpecsRange'] = f_lim # store settings in dict self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_range'}) #------------------------------------------------------------- def load_dict(self): """ Reload comboBox settings and textfields from filter dictionary Block signals during update of combobox / lineedit widgets """ self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) self.cmbUnits.blockSignals(True) idx = self.cmbUnits.findText( fb.fil[0]['freq_specs_unit']) # get and set self.cmbUnits.setCurrentIndex(idx) # index for freq. unit combo box self.cmbUnits.blockSignals(False) self.cmbFRange.blockSignals(True) idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType']) self.cmbFRange.setCurrentIndex(idx) # set frequency range self.cmbFRange.blockSignals(False) self.butSort.blockSignals(True) self.butSort.setChecked(fb.fil[0]['freq_specs_sort']) self.butSort.blockSignals(False) #------------------------------------------------------------- def _store_sort_flag(self): """ Store sort flag in filter dict and emit 'specs_changed':'f_sort' when sort button is checked. """ fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked() if self.butSort.isChecked(): self.sig_tx.emit({'sender': __name__, 'specs_changed': 'f_sort'})