Source code for pyfda.input_widgets.input_coeffs

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

"""
Widget for displaying and modifying filter coefficients
"""
import logging
logger = logging.getLogger(__name__)

import sys

from pyfda.libs.compat import (Qt, QtCore, QWidget, QLineEdit, QApplication,
                      QIcon, QSize, QTableWidget, QTableWidgetItem, QVBoxLayout,
                      pyqtSignal, QStyledItemDelegate, QColor, QBrush)
import numpy as np

import pyfda.filterbroker as fb # importing filterbroker initializes all its globals
from pyfda.libs.pyfda_lib import qstr, fil_save, safe_eval, pprint_log
from pyfda.libs.pyfda_qt_lib import qstyle_widget, qset_cmb_box, qget_cmb_box, qget_selected
from pyfda.libs.pyfda_io_lib import CSV_option_box, qtable2text, qtext2table

from pyfda.pyfda_rc import params
import pyfda.libs.pyfda_fix_lib as fx

from .input_coeffs_ui import Input_Coeffs_UI

# TODO: implement checking for complex-valued filters somewhere (pyfda_lib?),
#       h[n] detects complex data (although it isn't)
# TODO: Fixpoint coefficients do not properly convert complex -> float when saving
#       the filter?
# TODO: This ItemDelegate method displayText is called again and again when an
#        item is selected?!
# TODO: negative values for WI don't work correctly
#
# TODO: Filters need to be scaled properly, see e.g. http://radio.feld.cvut.cz/matlab/toolbox/filterdesign/normalize.html
#       http://www.ue.eti.pg.gda.pl/~wrona/lab_dsp/cw05/matlab/Help1.pdf

# TODO: convert to a proper Model-View-Architecture using QTableView?

classes = {'Input_Coeffs':'b,a'} #: Dict containing class name : display name

[docs]class ItemDelegate(QStyledItemDelegate): """ The following methods are subclassed to replace display and editor of the QTableWidget. - `displayText()` displays the data stored in the table in various number formats - `createEditor()` creates a line edit instance for editing table entries - `setEditorData()` pass data with full precision and in selected format to editor - `setModelData()` pass edited data back to model (`self.ba`) Editing the table triggers `setModelData()` but does not emit a signal outside this class, only the `ui.butSave` button is highlighted. When it is pressed, a signal with `'data_changed':'input_coeffs'` is produced in class `Input_Coeffs`. Additionally, a signal is emitted with `'view_changed':'q_coeff'` by `ui2qdict()`?! """ def __init__(self, parent): """ Pass instance `parent` of parent class (Input_Coeffs) """ super(ItemDelegate, self).__init__(parent) self.parent = parent # instance of the parent (not the base) class #============================================================================== # def paint(self, painter, option, index): # """ # Override painter # # painter: instance of QPainter # option: instance of QStyleOptionViewItemV4 # index: instance of QModelIndex # # see http://www.mimec.org/node/305 # """ # index_role = index.data(Qt.AccessibleDescriptionRole).toString() # # if index_role == QtCore.QLatin1String("separator"): # y = (option.rect.top() + option.rect.bottom()) / 2 # # painter.setPen(option.palette.color( QPalette.Active, QPalette.Dark ) ) # painter.drawLine(option.rect.left(), y, option.rect.right(), y ) # else: # # continue with the original `paint()` method # super(ItemDelegate, self).paint(painter, option, index) # #==============================================================================
[docs] def initStyleOption(self, option, index): """ Initialize `option` with the values using the `index` index. When the item (0,1) is processed, it is styled especially. All other items are passed to the original `initStyleOption()` which then calls `displayText()`. Afterwards, check whether an fixpoint overflow has occured and color item background accordingly. """ if index.row() == 0 and index.column() == 1: # a[0]: always 1 option.text = "1" # QString object option.font.setBold(True) option.displayAlignment = Qt.AlignRight | Qt.AlignCenter # see http://zetcode.com/gui/pyqt5/painting/ : option.backgroundBrush = QBrush(Qt.BDiagPattern)#QColor(100, 200, 100, 200)) option.backgroundBrush.setColor(QColor(100, 100, 100, 200)) # don't continue with default initStyleOption... display routine ends here else: # continue with the original `initStyleOption()` and call displayText() super(ItemDelegate, self).initStyleOption(option, index) # test whether fixpoint conversion during displayText() created an overflow: if self.parent.myQ.ovr_flag > 0: # Color item backgrounds with pos. Overflows red option.backgroundBrush = QBrush(Qt.SolidPattern) option.backgroundBrush.setColor(QColor(100, 0, 0, 80)) elif self.parent.myQ.ovr_flag < 0: # Color item backgrounds with neg. Overflows blue option.backgroundBrush = QBrush(Qt.SolidPattern) option.backgroundBrush.setColor(QColor(0, 0, 100, 80))
#============================================================================== # def paint(self, painter, option, index): # # """ # painter: instance of QPainter (default) # option: instance of QStyleOptionViewItemV4 # index: instance of QModelIndex # """ # logger.debug("Ovr_flag:".format(self.parent.myQ.ovr_flag)) # #option.backgroundBrush = QBrush(QColor(000, 100, 100, 200)) # lightGray # #option.backgroundBrush.setColor(QColor(000, 100, 100, 200)) # # continue with the original `paint()` method # #option.palette.setColor(QPalette.Window, QColor(Qt.red)) # #option.palette.setColor(QPalette.Base, QColor(Qt.green)) # super(ItemDelegate, self).paint(painter, option, index) # #painter.restore() # #==============================================================================
[docs] def text(self, item): """ Return item text as string transformed by self.displayText() """ # return qstr(item.text()) # convert to "normal" string return qstr(self.displayText(item.text(), QtCore.QLocale()))
[docs] def displayText(self, text, locale): """ Display `text` with selected fixpoint base and number of places text: string / QVariant from QTableWidget to be rendered locale: locale for the text The instance parameter myQ.ovr_flag is set to +1 or -1 for positive / negative overflows, else it is 0. """ data_str = qstr(text) # convert to "normal" string if self.parent.myQ.frmt == 'float': data = safe_eval(data_str, return_type='auto') # convert to float return "{0:.{1}g}".format(data, params['FMT_ba']) elif self.parent.myQ.frmt == 'dec' and self.parent.myQ.WF > 0: # decimal fixpoint representation with fractional part return "{0:.{1}g}".format(self.parent.myQ.float2frmt(data_str), params['FMT_ba']) else: return "{0:>{1}}".format(self.parent.myQ.float2frmt(data_str), self.parent.myQ.places)
# see: http://stackoverflow.com/questions/30615090/pyqt-using-qtextedit-as-editor-in-a-qstyleditemdelegate
[docs] def createEditor(self, parent, options, index): """ Neet to set editor explicitly, otherwise QDoubleSpinBox instance is created when space is not sufficient?! editor: instance of e.g. QLineEdit (default) index: instance of QModelIndex options: instance of QStyleOptionViewItemV4 """ line_edit = QLineEdit(parent) H = int(round(line_edit.sizeHint().height())) W = int(round(line_edit.sizeHint().width())) line_edit.setMinimumSize(QSize(W, H)) #(160, 25)); return line_edit
# def updateEditorGeometry(self, editor, option, index): # """ # Updates the editor for the item specified by index according to the option given # """ # super(ItemDelegate, self).updateEditorGeometry(editor, option, index) # default
[docs] def setEditorData(self, editor, index): """ Pass the data to be edited to the editor: - retrieve data with full accuracy from self.ba (in float format) - requantize data according to settings in fixpoint object - represent it in the selected format (int, hex, ...) editor: instance of e.g. QLineEdit index: instance of QModelIndex """ # data = qstr(index.data()) # get data from QTableWidget data_str = qstr(safe_eval(self.parent.ba[index.column()][index.row()], return_type="auto")) if self.parent.myQ.frmt == 'float': # floating point format: pass data with full resolution editor.setText(data_str) else: # fixpoint format with base: pass requantized data with required number of places editor.setText("{0:>{1}}".format(self.parent.myQ.float2frmt(data_str), self.parent.myQ.places))
[docs] def setModelData(self, editor, model, index): """ When editor has finished, read the updated data from the editor, convert it back to floating point format and store it in both the model (= QTableWidget) and in self.ba. Finally, refresh the table item to display it in the selected format (via `float2frmt()`). editor: instance of e.g. QLineEdit model: instance of QAbstractTableModel index: instance of QModelIndex """ # check for different editor environments if needed and provide a default: # if isinstance(editor, QtGui.QTextEdit): # model.setData(index, editor.toPlainText()) # elif isinstance(editor, QComboBox): # model.setData(index, editor.currentText()) # else: # super(ItemDelegate, self).setModelData(editor, model, index) if self.parent.myQ.frmt == 'float': data = safe_eval(qstr(editor.text()), self.parent.ba[index.column()][index.row()], return_type='auto') # raw data without fixpoint formatting else: data = self.parent.myQ.frmt2float(qstr(editor.text()), self.parent.myQ.frmt) # transform back to float model.setData(index, data) # store in QTableWidget # if the entry is complex, convert ba (list of arrays) to complex type if isinstance(data, complex): self.parent.ba[0] = self.parent.ba[0].astype(complex) self.parent.ba[1] = self.parent.ba[1].astype(complex) self.parent.ba[index.column()][index.row()] = data # store in self.ba qstyle_widget(self.parent.ui.butSave, 'changed') self.parent._refresh_table_item(index.row(), index.column()) # refresh table entry
###############################################################################
[docs]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() #------------------------------------------------------------------------------
[docs] 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._equalize_ba_length() 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 columns of float scalars). Data is displayed via `ItemDelegate.displayText()` in the number format set by `self.frmt`. The table dimensions are set according to the dimensions of `self.ba`: - self.ba[0] -> b coefficients - self.ba[1] -> a coefficients Called at the end of nearly every method. """ try: self.num_rows = max(len(self.ba[1]), len(self.ba[0])) except IndexError: self.num_rows = len(self.ba[0]) # logger.debug("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) #------------------------------------------------------------------------------
[docs] 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 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)) #------------------------------------------------------------------------------
[docs] 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()
#------------------------------------------------------------------------------
[docs] 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() #------------------------------------------------------------------------------
[docs] 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()
#------------------------------------------------------------------------------ if __name__ == '__main__': """ Test with python -m pyfda.input_widgets.input_coeffs """ from pyfda import pyfda_rc as rc app = QApplication(sys.argv) mainw = Input_Coeffs(None) app.setActiveWindow(mainw) app.setStyleSheet(rc.qss_rc) mainw.show() sys.exit(app.exec_())