Fork PyRPL on GitHub

Source code for pyrpl.widgets.module_widgets.iir_widget

"""
The Iir widget allows to dynamically select zeros and poles of the iir filter
"""
from .base_module_widget import ModuleWidget
from collections import OrderedDict
from qtpy import QtCore, QtWidgets
import pyqtgraph as pg
import numpy as np
import sys
from ... import APP

class MyGraphicsWindow(pg.GraphicsWindow):
    def __init__(self, title, parent):
        super(MyGraphicsWindow, self).__init__(title)
        self.parent = parent
        self.setToolTip("IIR transfer function: \n"
                        "----------------------\n"
                        "CTRL + Left click: add one more pole. \n"
                        "SHIFT + Left click: add one more zero\n"
                        "Left Click: select pole (other possibility: click on the '+j' labels below the graph)\n"
                        "Left/Right arrows: change imaginary part (frequency) of the current pole or zero\n"
                        "Up/Down arrows; change the real part (width) of the current pole or zero. \n"
                        "Poles are represented by 'X', zeros by 'O'")
        self.doubleclicked = False
        #APP.setDoubleClickInterval(300)  # default value (550) is fine
        self.mouse_clicked_timer = QtCore.QTimer()
        self.mouse_clicked_timer.setSingleShot(True)
        self.mouse_clicked_timer.setInterval(APP.doubleClickInterval())
        self.mouse_clicked_timer.timeout.connect(self.mouse_clicked)

    # see https://wiki.python.org/moin/PyQt/Distinguishing%20between%20click%20and%20double%20click
    # "The trick is to realise that Qt delivers MousePress, MouseRelease,
    # MouseDoubleClick and MouseRelease events in that order to the widget."
    def mousePressEvent(self, event):
        self.doubleclicked = False
        self.storeevent(event)
        if self.button == QtCore.Qt.LeftButton and self.modifier == 0:  # left button, no key
            self.parent.module.select_pole_or_zero(self.x)
        if not self.mouse_clicked_timer.isActive():
            self.mouse_clicked_timer.start()
        return super(MyGraphicsWindow, self).mousePressEvent(event)

    def mouseDoubleClickEvent(self, event):
        self.doubleclicked = True
        self.storeevent(event)
        if self.mouse_clicked_timer.isActive():
            self.mouse_clicked_timer.stop()
            self.mouse_clicked()
        return super(MyGraphicsWindow, self).mouseDoubleClickEvent(event)

    def storeevent(self, event):
        self.button = event.button()
        self.modifier = int(event.modifiers())
        it = self.getItem(0, 0)
        pos = it.mapToScene(event.pos()) #  + it.vb.pos()
        point = it.vb.mapSceneToView(pos)
        self.x, self.y = point.x(), point.y()
        if self.parent.xlog:
            self.x = 10 ** self.x  # takes logscale into account

    def mouse_clicked(self):
        # select nearest pole/zero with a simple click, even if something else is to happen after
        default_damping = self.x/10.0
        if self.button == QtCore.Qt.LeftButton:
            if self.doubleclicked:
                new = -default_damping - 1.j * self.x
                if self.modifier == QtCore.Qt.CTRL:
                    self.parent.module.complex_poles.append(new)
                if self.modifier == QtCore.Qt.SHIFT:
                    self.parent.module.complex_zeros.append(new)
            else:  # single click
                new = -self.x
                if self.modifier == 0:
                    pass # see above in mousePressEvent()
                if self.modifier == QtCore.Qt.CTRL:
                    # make a new real pole
                    self.parent.module.real_poles.append(new)
                if self.modifier == QtCore.Qt.SHIFT:
                    # make a new real zero
                    self.parent.module.real_zeros.append(new)

    def keyPressEvent(self, event):
        """ not working properly yet"""
        try:
            name = self.parent.module._selected_pole_or_zero
            index = self.parent.module._selected_index
            return self.parent.parent.attribute_widgets[name].widgets[index].keyPressEvent(event)
        except:
            return super(MyGraphicsWindow, self).keyPressEvent(event)

    def keyReleaseEvent(self, event):
        """ not working properly yet"""
        def keyPressEvent(self, event):
            try:
                name = self.parent.module._selected_pole_or_zero
                index = self.parent.module._selected_index
                return self.parent.parent.attribute_widgets[name].widgets[index].keyReleaseEvent(event)
            except:
                return super(MyGraphicsWindow, self).keyReleaseEvent(event)


[docs]class IirGraphWidget(QtWidgets.QGroupBox): # whether xaxis is plotted in log-scale xlog = True def __init__(self, parent): # graph self.name = "Transfer functions" super(IirGraphWidget, self).__init__(parent) self.parent = parent self.module = self.parent.module self.layout = QtWidgets.QVBoxLayout(self) self.win = MyGraphicsWindow(title="Amplitude", parent=self) self.win_phase = MyGraphicsWindow(title="Phase", parent=self) # self.proxy = pg.SignalProxy(self.win.scene().sigMouseClicked, # rateLimit=60, slot=self.mouse_clicked) self.mag = self.win.addPlot(title="Magnitude (dB)") self.phase = self.win_phase.addPlot(title="Phase (deg)") self.phase.setXLink(self.mag) # self.proxy_phase = pg.SignalProxy(self.win_phase.scene().sigMouseClicked, # rateLimit=60, slot=self.mouse_clicked) # we will plot the following curves: # poles, zeros -> large dots and crosses # design (designed filter) - yellow line # measured filter with na - orange dots <- # data (measruement data) - green line # data x design (or data/design) - red line self.plots = OrderedDict() # make lines for name, style in [('data', dict(pen='g')), ('filter_design', dict(pen='y')), ('data_x_design', dict(pen='r'))]: self.plots[name] = self.mag.plot(**style) self.plots[name + "_phase"] = self.phase.plot(**style) self.plots[name].setLogMode(xMode=self.xlog, yMode=None) self.plots[name + '_phase'].setLogMode(xMode=self.xlog, yMode=None) for name, style in [('filter_measurement', dict(symbol='o', size=10, pen='b')), ('zeros', dict(pen=pg.mkPen(None), symbol='o', size=10, brush=pg.mkBrush(255, 0, 255, 120))), ('poles', dict(size=15, symbol='x', pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 255, 120))), # ('actpole', dict(size=30, # symbol='x', # pen='r', # brush=pg.mkBrush(255, 0, 255, # 120))), # ('actzero', dict(size=30, # symbol='o', # pen='r', # brush=pg.mkBrush(255, 0, 255, # 120))) ]: item = pg.ScatterPlotItem(**style) self.mag.addItem(item) self.plots[name] = item item = pg.ScatterPlotItem(**style) self.phase.addItem(item) self.plots[name+'_phase'] = item # also set logscale for the xaxis # make scatter plots self.mag.setLogMode(x=self.xlog, y=None) self.phase.setLogMode(x=self.xlog, y=None) self.layout.addWidget(self.win) self.layout.addWidget(self.win_phase)
# connect signals #self.plots['poles'].sigClicked.connect(self.parent.select_pole) #self.plots['poles_phase'].sigClicked.connect(self.parent.select_pole) #self.plots['zeros'].sigClicked.connect(self.parent.select_zero) #self.plots['zeros_phase'].sigClicked.connect(self.parent.select_zero)
[docs]class IirButtonWidget(QtWidgets.QGroupBox): BUTTONWIDTH = 100 def __init__(self, parent): # buttons and standard attributes self.name = "General settings" super(IirButtonWidget, self).__init__(parent) self.parent = parent self.module = self.parent.module self.layout = QtWidgets.QVBoxLayout(self) #self.setLayout(self.layout) # wasnt here before aws = self.parent.attribute_widgets for attr in ['input', 'inputfilter', 'output_direct', 'loops', 'gain', 'on', 'bypass', 'overflow']: widget = aws[attr] widget.setFixedWidth(self.BUTTONWIDTH) self.layout.addWidget(widget) self.setFixedWidth(self.BUTTONWIDTH+50)
[docs]class IirBottomWidget(QtWidgets.QGroupBox): BUTTONWIDTH = 300 def __init__(self, parent): # widget for poles and zeros self.name = "Filter poles and zeros" super(IirBottomWidget, self).__init__(parent) self.parent = parent self.module = self.parent.module self.layout = QtWidgets.QHBoxLayout(self) #self.setLayout(self.layout) # wasnt here before aws = self.parent.attribute_widgets for attr in ['complex_poles', 'complex_zeros', 'real_poles', 'real_zeros']: widget = aws[attr] widget.setFixedWidth(self.BUTTONWIDTH) self.layout.addWidget(widget)
[docs]class IirWidget(ModuleWidget):
[docs] def init_gui(self): # setup filter in its present state self.module.setup() # moved at the beginning of the function, # otherwise, values altered in setup (such as iir.loops) are # not updated in the gui (gui already creted but not yet connected # to the signal launcher) self.init_main_layout(orientation="vertical") #self.main_layout = QtWidgets.QVBoxLayout() #self.setLayout(self.main_layout) # add all attribute widgets and remove them right away self.init_attribute_layout() for widget in self.attribute_widgets.values(): self.main_layout.removeWidget(widget) # divide into top and bottom layout self.top_layout = QtWidgets.QHBoxLayout() self.main_layout.addLayout(self.top_layout) # add graph widget self.graph_widget = IirGraphWidget(self) self.top_layout.addWidget(self.graph_widget) # buttons to the right of graph self.button_widget = IirButtonWidget(self) self.top_layout.addWidget(self.button_widget) # poles and zeros at the bottom of the graph self.bottom_widget = IirBottomWidget(self) self.main_layout.addWidget(self.bottom_widget) # set colors of labels to the one of the corresponding traces self.attribute_widgets['data_curve'].setStyleSheet("color: green") self.update_plot()
[docs] def select_pole(self, plot_item, spots): index = spots[0].data() self.attribute_widgets['poles'].set_selected(index)
[docs] def select_zero(self, plot_item, spots): index = spots[0].data() self.attribute_widgets['zeros'].set_selected(index)
@property def frequencies(self): try: f = self.module._data_curve_object.data.index.values except AttributeError: # in case data_curve is None (no curve selected) return np.logspace(1, np.log10(5e6), 2000) else: # avoid zero frequency (log plot) f[f<=0] = sys.float_info.epsilon return np.asarray(f, dtype=float) def _magnitude(self, data): return 20. * np.log10(np.abs(np.asarray(data, dtype=np.complex)) + sys.float_info.epsilon) def _phase(self, data): return np.angle(np.asarray(data, dtype=np.complex), deg=True)
[docs] def update_plot(self): # first, we compile the line plot data, then we iterate over them and # plot them. we then plot the scatter plots in the same manner tfargs = {} # args to the call of iir.transfer_function frequencies = self.frequencies plot = OrderedDict() # plot underlying curve data try: plot['data'] = self.module._data_curve_object.data.values except AttributeError: # no curve for plotting available plot['data'] = [] # plot designed filter plot['filter_design'] = self.module.transfer_function(frequencies, **tfargs) # plot product try: plot['data_x_design'] = plot['data'] / plot['filter_design'] except ValueError: try: plot['data_x_design'] = 1.0 / plot['filter_design'] except: plot['data_x_design'] = [] # plot everything (all lines) up to here for k, v in plot.items(): self.graph_widget.plots[k].setData(frequencies[:len(v)], self._magnitude(v)) self.graph_widget.plots[k+'_phase'].setData(frequencies[:len(v)], self._phase(v)) # plot poles and zeros aws = self.attribute_widgets for end in ['poles', 'zeros']: mag, phase = [], [] for start in ['complex', 'real']: key = start+'_'+end freq = getattr(self.module, key) if start == 'complex': freq = np.imag(freq) freq = np.abs(freq) tf = self.module.transfer_function(freq, **tfargs) selected = aws[key].attribute_value.selected brush = [pg.mkBrush(color='b') if (num == selected) else pg.mkBrush(color='y') for num in range(aws[key].number)] mag += [{'pos': (fr, val), 'data': i, 'brush': br} for (i, (fr, val, br)) in enumerate(zip(list(np.log10(freq)), list(self._magnitude(tf)), brush))] phase += [{'pos': (fr, val), 'data': i, 'brush': br} for (i, (fr, val, br)) in enumerate(zip(list(np.log10(freq)), list(self._phase(tf)), brush))] self.graph_widget.plots[end].setPoints(mag) self.graph_widget.plots[end+'_phase'].setPoints(phase)
[docs] def keyPressEvent(self, event): """ not working properly yet""" try: name = self.module._selected_pole_or_zero index = self.module._selected_index return self.attribute_widgets[name].widgets[index].keyPressEvent(event) except: return super(MyGraphicsWindow, self).keyPressEvent(event)
[docs] def keyReleaseEvent(self, event): """ not working properly yet""" def keyPressEvent(self, event): try: name = self.module._selected_pole_or_zero index = self.module._selected_index return self.attribute_widgets[name].widgets[index].keyReleaseEvent(event) except: return super(MyGraphicsWindow, self).keyReleaseEvent(event)