Source code for pyfda.input_widgets.input_pz

# -*- coding: utf-8 -*-
# This file is part of the pyFDA project hosted at
# 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 Poles and Zeros
import logging
logger = logging.getLogger(__name__)

import sys
import re
from pprint import pformat

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

from pyfda.libs.pyfda_qt_lib import qget_cmb_box, qstyle_widget
from pyfda.libs.pyfda_io_lib import qtable2text, qtext2table

import numpy as np
from scipy.signal import freqz, zpk2tf

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.pyfda_rc import params

from pyfda.input_widgets.input_pz_ui import Input_PZ_UI

classes = {'Input_PZ':'P/Z'} #: 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.zpk`) """ def __init__(self, parent): """ Pass instance `parent` of parent class (Input_PZ) """ super(ItemDelegate, self).__init__(parent) self.parent = parent # instance of the parent (not the base) class
[docs] def initStyleOption(self, option, index): """ Initialize `option` with the values using the `index` index. All items are passed to the original `initStyleOption()` which then calls `displayText()`. Afterwards, check whether a pole (index.column() == 1 )is outside the UC and color item background accordingly (not implemented yet). """ # continue with the original `initStyleOption()` and call displayText() super(ItemDelegate, self).initStyleOption(option, index) # test whether fixpoint conversion during displayText() created an overflow: if index.column() == 1 and False: # Color item backgrounds with pos. Overflows red option.backgroundBrush = QBrush(Qt.SolidPattern) option.backgroundBrush.setColor(QColor(100, 0, 0, 80))
[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 format (cartesian / polar - to be implemented) and number of places text: string / QVariant from QTableWidget to be rendered locale: locale for the text """ return self.parent.cmplx2frmt(text, places=params['FMT_pz'])
#r, phi = np.absolute(data), np.angle(data, deg=False) #return "{0:.{2}g} * {3}{1:.{2}g} rad".format(r, phi, params['FMT_pz'], self.angle_char)
[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
[docs] def setEditorData(self, editor, index): """ Pass the data to be edited to the editor: - retrieve data with full accuracy (`places=-1`) from `zpk` (in float format) - represent it in the selected format (Cartesian, polar, ...) editor: instance of e.g. QLineEdit index: instance of QModelIndex """ # data = qstr( # get data from QTableWidget data = self.parent.zpk[index.column()][index.row()] data_str = self.parent.cmplx2frmt(data, places=-1)# qstr(safe_eval(data, return_type="auto")) editor.setText(data_str)
[docs] def setModelData(self, editor, model, index): """ When editor has finished, read the updated data from the editor, convert it to complex format and store it in both the model (= QTableWidget) and in `zpk`. Finally, refresh the table item to display it in the selected format (via `to be defined`) and normalize the gain. 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) # convert entered string to complex, pass the old value as default data = self.parent.frmt2cmplx(qstr(editor.text()), self.parent.zpk[index.column()][index.row()]) model.setData(index, data) # store in QTableWidget self.parent.zpk[index.column()][index.row()] = data # and in qstyle_widget(self.parent.ui.butSave, 'changed') self.parent._refresh_table_item(index.row(), index.column()) # refresh table entry self.parent._normalize_gain() # recalculate gain
[docs]class ItemDelegateAnti(QStyledItemDelegate): """ The following methods are subclassed to replace display and editor of the QTableWidget. `displayText()` displays number with n_digits without sacrificing precision of the data stored in the table. """ def __init__(self, parent): """ Pass instance `parent` of parent class (Input_PZ) """ super(ItemDelegateAnti, self).__init__(parent) self.parent = parent # instance of the parent (not the base) class
[docs] def displayText(self, text, locale): return "{:.{n_digits}g}".format(safe_eval(qstr(text), return_type='cmplx'), n_digits = params['FMT_pz'])
[docs]class Input_PZ(QWidget): """ Create the window for entering exporting / importing and saving / loading data """ sig_rx = pyqtSignal(object) # incoming from input_tab_widgets sig_tx = pyqtSignal(object) # emitted when filter has been saved def __init__(self, parent): super(Input_PZ, self).__init__(parent) self.data_changed = True # initialize flag: filter data has been changed self.Hmax_last = 1 # initial setting for maximum gain self.angle_char = "\u2220" self.tab_label = "P/Z" self.tool_tip = "Display and edit filter poles and zeros." self.ui = Input_PZ_UI(self) # create the UI part with buttons etc. self.norm_last = qget_cmb_box(self.ui.cmbNorm, data=False) # initial setting of cmbNorm self._construct_UI() # construct the rest of the UI self.load_dict() # initialize table from filterbroker self._refresh_table() # initialize table with values self.setup_signal_slot() # setup signal-slot connections and eventFilters #------------------------------------------------------------------------------
[docs] def process_sig_rx(self, dict_sig=None): """ Process signals coming from sig_rx """ if dict_sig['sender'] == __name__: logger.debug("Stopped infinite loop:\n{0}".format(pprint_log(dict_sig))) return else: logger.debug("SIG_RX - data_changed = {0}, vis = {1}\n{2}"\ .format(self.data_changed, self.isVisible(), pprint_log(dict_sig))) if 'ui_changed' in dict_sig and dict_sig['ui_changed'] == 'csv': self.ui._set_load_save_icons() #self.sig_tx.emit(dict_sig) elif self.isVisible(): if 'data_changed' in dict_sig or self.data_changed: self.load_dict() self.data_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
#------------------------------------------------------------------------------ def _construct_UI(self): """ Intitialize the widget """ self.tblPZ = QTableWidget(self) # self.tblPZ.setEditTriggers(QTableWidget.AllEditTriggers) # make everything editable self.tblPZ.setAlternatingRowColors(True) # alternating row colors) self.tblPZ.setObjectName("tblPZ") self.tblPZ.horizontalHeader().setHighlightSections(True) # highlight when selected self.tblPZ.horizontalHeader().setFont(self.ui.bfont) self.tblPZ.verticalHeader().setHighlightSections(True) self.tblPZ.verticalHeader().setFont(self.ui.bfont) self.tblPZ.setColumnCount(2) self.tblPZ.setItemDelegate(ItemDelegate(self)) layVMain = QVBoxLayout() layVMain.setAlignment(Qt.AlignTop) # this affects only the first widget (intended here) layVMain.addWidget(self.ui) layVMain.addWidget(self.tblPZ) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain)
[docs] def setup_signal_slot(self): """ Setup setup signal-slot connections """ #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) self.ui.sig_tx.connect(self.sig_tx) #---------------------------------------------------------------------- # LOCAL (UI) SIGNALS & SLOTs #---------------------------------------------------------------------- self.ui.cmbPZFrmt.activated.connect(self._refresh_table) self.ui.spnDigits.editingFinished.connect(self._refresh_table) self.ui.butLoad.clicked.connect(self.load_dict) self.ui.butEnable.clicked.connect(self.load_dict) self.ui.butSave.clicked.connect(self._save_entries) self.ui.cmbNorm.activated.connect(self._normalize_gain) self.ui.butDelCells.clicked.connect(self._delete_cells) self.ui.butAddCells.clicked.connect(self._add_rows) self.ui.butClear.clicked.connect(self._clear_table) self.ui.butFromTable.clicked.connect(self._copy_from_table) self.ui.butToTable.clicked.connect(self._copy_to_table) self.ui.butSetZero.clicked.connect(self._zero_PZ) self.ui.ledGain.installEventFilter(self) self.ui.ledEps.editingFinished.connect(self._set_eps)
#---------------------------------------------------------------------- # self.tblPZ.itemSelectionChanged.connect(self._copy_item) # # Every time a table item is edited, call self._copy_item to copy the # item content to self.zpk. This is triggered by the itemChanged signal. # The event filter monitors the focus of the input fields. # signal itemChanged is also triggered programmatically, # itemSelectionChanged is only triggered when entering cell #------------------------------------------------------------------------------
[docs] 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): if event.type() == QEvent.FocusIn: # 8 self.spec_edited = False self._restore_gain(source) return True # event processing stops here elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() # key press: 6, key release: 7 if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: # store entry self._store_gain(source) self._restore_gain(source) # display in desired format return True elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False self._restore_gain(source) return True elif event.type() == QEvent.FocusOut: # 9 self._store_gain(source) self._restore_gain(source) # display in desired format return True return super(Input_PZ, self).eventFilter(source, event)
#------------------------------------------------------------------------------ def _store_gain(self, source): """ When the textfield of `source` has been edited (flag `self.spec_edited` = True), store it in the shadow dict. This is triggered by `QEvent.focusOut` or RETURN key. """ if self.spec_edited: self.zpk[2] = safe_eval(source.text(), alt_expr = str(self.zpk[2])) qstyle_widget(self.ui.butSave, 'changed') self.spec_edited = False # reset flag #------------------------------------------------------------------------------ def _normalize_gain(self): """ Normalize the gain factor so that the maximum of |H(f)| stays 1 or a previously stored maximum value of |H(f)|. Do this every time a P or Z has been changed. Called by setModelData() and when cmbNorm is activated """ norm = qget_cmb_box(self.ui.cmbNorm, data=False) self.ui.ledGain.setEnabled(norm == 'None') if norm != self.norm_last: qstyle_widget(self.ui.butSave, 'changed') if not np.isfinite(self.zpk[2]): self.zpk[2] = 1. self.zpk[2] = np.real_if_close(self.zpk[2]).item() if np.iscomplex(self.zpk[2]): logger.warning("Casting complex to real for gain k!") self.zpk[2] = np.abs(self.zpk[2]) if norm != "None": b, a = zpk2tf(self.zpk[0], self.zpk[1], self.zpk[2]) [w, H] = freqz(b, a, whole=True) Hmax = max(abs(H)) if not np.isfinite(Hmax) or Hmax > 1e4 or Hmax < 1e-4: Hmax = 1. if norm == "1": self.zpk[2] = self.zpk[2] / Hmax # normalize to 1 elif norm == "Max": if norm != self.norm_last: # setting has been changed -> 'Max' self.Hmax_last = Hmax # use current design to set Hmax_last self.zpk[2] = self.zpk[2] / Hmax * self.Hmax_last self.norm_last = norm # store current setting of combobox self._restore_gain() #------------------------------------------------------------------------------ def _restore_gain(self, source = None): """ Update QLineEdit with either full (has focus) or reduced precision (no focus) Called by eventFilter, _normalize_gain() and _refresh_table() """ if self.ui.butEnable.isChecked(): if len(self.zpk) == 3: pass elif len(self.zpk) == 2: # k is missing in zpk: self.zpk.append(1.) # use k = 1 else: logger.error("P/Z list zpk has wrong length {0}".format(len(self.zpk))) k = safe_eval(self.zpk[2], return_type='auto') if not self.ui.ledGain.hasFocus(): # no focus, round the gain self.ui.ledGain.setText(str(params['FMT'].format(k))) else: # widget has focus, show gain with full precision self.ui.ledGain.setText(str(k)) #------------------------------------------------------------------------------ def _refresh_table_item(self, row, col): """ Refresh the table item with the index `row, col` from self.zpk """ item = self.tblPZ.item(row, col) if item: # does item exist? item.setText(str(self.zpk[col][row]).strip('()')) else: # no, construct it: self.tblPZ.setItem(row,col,QTableWidgetItem( str(self.zpk[col][row]).strip('()'))) self.tblPZ.item(row, col).setTextAlignment(Qt.AlignRight|Qt.AlignCenter) #------------------------------------------------------------------------------ def _refresh_table(self): """ (Re-)Create the displayed table from self.zpk with the desired number format. TODO: Update zpk[2]? Called by: load_dict(), _clear_table(), _zero_PZ(), _delete_cells(), add_row(), _copy_to_table() """ params['FMT_pz'] = int(self.ui.spnDigits.text()) self.tblPZ.setVisible(self.ui.butEnable.isChecked()) if self.ui.butEnable.isChecked(): self.ui.butEnable.setIcon(QIcon(':/circle-x.svg')) self._restore_gain() self.tblPZ.setHorizontalHeaderLabels(["Zeros", "Poles"]) self.tblPZ.setRowCount(max(len(self.zpk[0]),len(self.zpk[1]))) self.tblPZ.blockSignals(True) for col in range(2): for row in range(len(self.zpk[col])): self._refresh_table_item(row, col) self.tblPZ.blockSignals(False) self.tblPZ.resizeColumnsToContents() self.tblPZ.resizeRowsToContents() self.tblPZ.clearSelection() else: # disable widgets self.ui.butEnable.setIcon(QIcon(':/circle-check.svg')) #------------------------------------------------------------------------------
[docs] def load_dict(self): """ Load all entries from filter dict fb.fil[0]['zpk'] into the Zero/Pole/Gain list self.zpk and update the display via `self._refresh_table()`. The explicit np.array( ... ) statement enforces a deep copy of fb.fil[0], otherwise the filter dict would be modified inadvertedly. `dtype=object` needs to be specified to create a numpy array from the nested lists with differing lengths without creating the deprecation warning "Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated." The filter dict fb.fil[0]['zpk'] is a list of numpy float ndarrays for z / p / k values `self.zpk` is an array of float ndarrays with different lengths of z / p / k subarrays to allow adding / deleting items. """ self.zpk = np.array(fb.fil[0]['zpk'], dtype=object)# this enforces a deep copy qstyle_widget(self.ui.butSave, 'normal') self._refresh_table()
#------------------------------------------------------------------------------ def _save_entries(self): """ Save the values from self.zpk to the filter PZ dict, the QLineEdit for setting the gain has to be treated separately. """ logger.debug("_save_entries called") fb.fil[0]['N'] = len(self.zpk[0]) if np.any(self.zpk[1]): # any non-zero poles? fb.fil[0]['fc'] = 'Manual_IIR' else: fb.fil[0]['fc'] = 'Manual_FIR' try: fil_save(fb.fil[0], self.zpk, 'zpk', __name__) # save with new gain except Exception as e: # catch exception due to malformatted P/Zs: logger.error("While saving the poles / zeros, " "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_pz'}) # -> input_tab_widgets qstyle_widget(self.ui.butSave, 'normal') logger.debug("b,a = {0}\n\n" "zpk = {1}\n" .format(pformat(fb.fil[0]['ba']), pformat(fb.fil[0]['zpk']) )) #------------------------------------------------------------------------------ def _clear_table(self): """ Clear & initialize table and zpk for two poles and zeros @ origin, P = Z = [0; 0], k = 1 """ self.zpk = np.array([[0, 0], [0, 0], 1], dtype=object) self.Hmax_last = 1.0 self.anti = False qstyle_widget(self.ui.butSave, 'changed') self._refresh_table() #------------------------------------------------------------------------------ def _get_selected(self, table): """ get selected cells and return: - indices of selected cells - selected colums - selected rows - current cell """ idx = [] for _ in table.selectedItems(): idx.append([_.column(), _.row(), ]) cols = sorted(list({i[0] for i in idx})) rows = sorted(list({i[1] for i in idx})) cur = (table.currentColumn(), table.currentRow()) #cur_idx_row = table.currentIndex().row() return {'idx':idx, 'cols':cols, 'rows':rows, 'cur':cur} #------------------------------------------------------------------------------ def _delete_cells(self): """ Delete all selected elements by: - determining the indices of all selected cells in the P and Z arrays - deleting elements with those indices - equalizing the lengths of P and Z array by appending the required number of zeros. - deleting all P/Z pairs Finally, the table is refreshed from self.zpk. """ sel = self._get_selected(self.tblPZ)['idx'] # get all selected indices Z = [s[1] for s in sel if s[0] == 0] # all selected indices in 'Z' column P = [s[1] for s in sel if s[0] == 1] # all selected indices in 'P' column # Delete array entries with selected indices. If nothing is selected # (Z and P are empty), delete the last row. if len(Z) < 1 and len(P) < 1: Z = [len(self.zpk[0])-1] P = [len(self.zpk[1])-1] self.zpk[0] = np.delete(self.zpk[0], Z) self.zpk[1] = np.delete(self.zpk[1], P) # test and equalize if P and Z array have different lengths: D = len(self.zpk[0]) - len(self.zpk[1]) if D > 0: self.zpk[1] = np.append(self.zpk[1], np.zeros(D)) elif D < 0: self.zpk[0] = np.append(self.zpk[0], np.zeros(-D)) self._delete_PZ_pairs() self._normalize_gain() qstyle_widget(self.ui.butSave, 'changed') self._refresh_table() #------------------------------------------------------------------------------ def _add_rows(self): """ Add the number of selected rows to the table and fill new cells with zeros. If nothing is selected, add one row. """ row = self.tblPZ.currentRow() sel = len(self._get_selected(self.tblPZ)['rows']) # TODO: evaluate and create non-contiguous selections as well? if sel == 0: # nothing selected -> sel = 1 # add at least one row ... row = min(len(self.zpk[0]), len(self.zpk[1])) # ... at the bottom self.zpk[0] = np.insert(self.zpk[0], row, np.zeros(sel)) self.zpk[1] = np.insert(self.zpk[1], row, np.zeros(sel)) self._refresh_table() #------------------------------------------------------------------------------ def _set_eps(self): """ Set tolerance value """ self.ui.eps = safe_eval(self.ui.ledEps.text(), alt_expr=self.ui.eps, sign='pos') self.ui.ledEps.setText(str(self.ui.eps)) #------------------------------------------------------------------------------ def _zero_PZ(self): """ Set all P/Zs = 0 with a magnitude less than eps and delete P/Z pairs afterwards. """ changed = False targ_val = 0. test_val = 0 sel = self._get_selected(self.tblPZ)['idx'] # get all selected indices if not sel: # nothing selected, check all cells z_close = np.logical_and(np.isclose(self.zpk[0], test_val, rtol=0, atol = self.ui.eps), (self.zpk[0] != targ_val)) p_close = np.logical_and(np.isclose(self.zpk[1], test_val, rtol=0, atol = self.ui.eps), (self.zpk[1] != targ_val)) if z_close.any(): self.zpk[0] = np.where(z_close, targ_val, self.zpk[0]) changed = True if p_close.any(): self.zpk[1] = np.where(p_close, targ_val, self.zpk[1]) changed = True else: for i in sel: # check only selected cells if np.logical_and(np.isclose(self.zpk[i[0]][i[1]], test_val, rtol=0, atol = self.ui.eps), (self.zpk[i[0]][i[1]] != targ_val)): self.zpk[i[0]][i[1]] = targ_val changed = True self._delete_PZ_pairs() self._normalize_gain() if changed: qstyle_widget(self.ui.butSave, 'changed') # mark save button as changed self._refresh_table() #------------------------------------------------------------------------------ def _delete_PZ_pairs(self): """ Find and delete pairs of poles and zeros in self.zpk The filter dict and the table have to be updated afterwards. """ for z in range(len(self.zpk[0])-1, -1, -1): # start at the bottom for p in range(len(self.zpk[1])-1, -1, -1): if np.isclose(self.zpk[0][z], self.zpk[1][p], rtol = 0, atol = self.ui.eps): self.zpk[0] = np.delete(self.zpk[0], z) self.zpk[1] = np.delete(self.zpk[1], p) break # ... out of loop if len(self.zpk[0]) < 1 : # no P / Z, add 1 row self.zpk[0] = np.append(self.zpk[0], 0.) self.zpk[1] = np.append(self.zpk[1], 0.) #------------------------------------------------------------------------------
[docs] def cmplx2frmt(self, text, places=-1): """ Convert number "text" (real or complex or string) to the format defined by cmbPZFrmt. Returns: string """ # convert to "normal" string and prettify via safe_eval: data = safe_eval(qstr(text), return_type='auto') frmt = qget_cmb_box(self.ui.cmbPZFrmt) # get selected format if places == -1: full_prec = True else: full_prec = False if frmt == 'cartesian' or not (type(data) == complex): if full_prec: return "{0}".format(data) else: return "{0:.{plcs}g}".format(data, plcs=places) elif frmt == 'polar_rad': r, phi = np.absolute(data), np.angle(data, deg=False) if full_prec: return "{r} * {angle_char}{p} rad"\ .format(r=r, p=phi, angle_char=self.angle_char) else: return "{r:.{plcs}g} * {angle_char}{p:.{plcs}g} rad"\ .format(r=r, p=phi, plcs=places, angle_char=self.angle_char) elif frmt == 'polar_deg': r, phi = np.absolute(data), np.angle(data, deg=True) if full_prec: return "{r} * {angle_char}{p}°"\ .format(r=r, p=phi, angle_char=self.angle_char) else: return "{r:.{plcs}g} * {angle_char}{p:.{plcs}g}°"\ .format(r=r, p=phi, plcs=places, angle_char=self.angle_char) elif frmt == 'polar_pi': r, phi = np.absolute(data), np.angle(data, deg=False) / np.pi if full_prec: return "{r} * {angle_char}{p} pi"\ .format(r=r, p=phi, angle_char=self.angle_char) else: return "{r:.{plcs}g} * {angle_char}{p:.{plcs}g} pi"\ .format(r=r, p=phi, plcs=places, angle_char=self.angle_char) else: logger.error("Unknown format {0}.".format(frmt))
[docs] def frmt2cmplx(self, text, default=0.): """ Convert format defined by cmbPZFrmt to real or complex """ conv_error = False text = qstr(text).replace(" ", "") # convert to "proper" string without blanks if qget_cmb_box(self.ui.cmbPZFrmt) == 'cartesian': return safe_eval(text, default, return_type='auto') else: polar_str = text.split('*' + self.angle_char, 1) if len(polar_str) < 2: # input is real or imaginary r = safe_eval(re.sub('['+self.angle_char+'<∠°]','', text), default, return_type='auto') x = r.real y = r.imag else: r = safe_eval(polar_str[0], sign='pos') if safe_eval.err > 0: conv_error = True if "°" in polar_str[1]: scale = np.pi / 180. # angle in degrees elif'π$|pi$', polar_str[1]): scale = np.pi else: scale = 1. # angle in rad # remove right-most special characters (regex $) polar_str[1] = re.sub('['+self.angle_char+'<∠°π]$|rad$|pi$', '', polar_str[1]) phi = safe_eval(polar_str[1]) * scale if safe_eval.err > 0: conv_error = True if not conv_error: x = r * np.cos(phi) y = r * np.sin(phi) else: x = default.real y = default.imag logger.error("Expression {0} could not be evaluated.".format(text)) return x + 1j * y
#------------------------------------------------------------------------------ def _copy_from_table(self): """ Copy data from coefficient table `self.tblCoeff` to clipboard in CSV format or to file using a selected format """ # pass table instance, numpy data and current class for accessing the # clipboard instance or for constructing a QFileDialog instance qtable2text(self.tblPZ, self.zpk, self, 'zpk', title="Export Poles / Zeros") #------------------------------------------------------------------------------ def _copy_to_table(self): """ Read data from clipboard / file and copy it to `self.zpk` as array of complex # TODO: More checks for swapped row <-> col, single values, wrong data type ... """ data_str = qtext2table(self, 'zpk', title="Import Poles / Zeros ") if data_str is None: # file operation has been aborted return conv = self.frmt2cmplx # routine for converting to cartesian coordinates 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.debug("_copy_to_table: c x r:", num_cols, num_rows) if orientation_horiz: self.zpk = [[],[]] for c in range(num_cols): self.zpk[0].append(conv(data_str[c][0])) if num_rows > 1: self.zpk[1].append(conv(data_str[c][1])) else: self.zpk[0] = [conv(s) for s in data_str[0]] if num_cols > 1: self.zpk[1] = [conv(s) for s in data_str[1]] else: self.zpk[1] = [1] self.zpk[0] = np.asarray(self.zpk[0]) self.zpk[1] = np.asarray(self.zpk[1]) self._equalize_columns() qstyle_widget(self.ui.butSave, 'changed') self._refresh_table() #------------------------------------------------------------------------------ def _equalize_columns(self): """ test and equalize if P and Z subarray have different lengths: """ try: p_len = len(self.zpk[1]) except IndexError: p_len = 0 try: z_len = len(self.zpk[0]) except IndexError: z_len = 0 D = z_len - p_len if D > 0: # more zeros than poles self.zpk[1] = np.append(self.zpk[1], np.zeros(D)) elif D < 0: # more poles than zeros self.zpk[0] = np.append(self.zpk[0], np.zeros(-D))
# if fb.fil[0]['ft'] == 'IIR': # self.zpk[0] = np.append(self.zpk[0], np.zeros(-D)) # else: # self.zpk[1] = self.zpk[1][:D] # discard last D elements of a #------------------------------------------------------------------------------ if __name__ == '__main__': """ Test with python -m pyfda.input_widgets.input_pz""" from pyfda import pyfda_rc as rc app = QApplication(sys.argv) app.setStyleSheet(rc.qss_rc) mainw = Input_PZ(None) app.setActiveWindow(mainw) sys.exit(app.exec_())