class ScansPanel(Panel): """Provides a display for the scans of the current experiment. Options: * ``fit_functions`` (default {}) -- dictionary for special fitting functions. The name of the fit function is followed by a set of a list of fit parameters and the string containing the fit function code:: fit_functions = { 'Resonance': (['Vmax = 0.1', 'R = 0.6', 'f = ', 'L = ', 'C = '], 'Vmax / sqrt(R**2 + (f*L-1/(f*C))**2)'), } The function can use the all mathematical functions of the `numpy <http://www.numpy.org/>`_ package. """ panelName = 'Scans' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/scans.ui') ArbitraryFitter.arby_functions.update(options.get('fit_functions', {})) self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.layout().addWidget(self.statusBar) self.x_menu = QMenu(self) self.x_menu.aboutToShow.connect(self.on_x_menu_aboutToShow) self.actionXAxis.setMenu(self.x_menu) self.y_menu = QMenu(self) self.y_menu.aboutToShow.connect(self.on_y_menu_aboutToShow) self.actionYAxis.setMenu(self.y_menu) self.actionAutoDisplay.setChecked(True) self.norm_menu = QMenu(self) self.norm_menu.aboutToShow.connect(self.on_norm_menu_aboutToShow) self.actionNormalized.setMenu(self.norm_menu) quickfit = QShortcut(QKeySequence("G"), self) quickfit.activated.connect(self.on_quickfit) self.user_color = Qt.white self.user_font = QFont('Monospace') self.bulk_adding = False self.no_openset = False self.last_norm_selection = None self.fitclass = GaussFitter self.fitfuncmap = {} self.menus = None self.bars = None self.data = self.mainwindow.data # maps set uid -> plot self.setplots = {} # maps set uid -> list item self.setitems = {} # current plot object self.currentPlot = None # stack of set uids self.setUidStack = [] # uids of automatically combined datasets -> uid of combined one self.contSetUids = {} self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if self.tablecolwidth0 > 0: self.metaTable.setColumnWidth(0, self.tablecolwidth0) self.metaTable.setColumnWidth(1, self.tablecolwidth1) self.data.datasetAdded.connect(self.on_data_datasetAdded) self.data.pointsAdded.connect(self.on_data_pointsAdded) self.data.fitAdded.connect(self.on_data_fitAdded) client.experiment.connect(self.on_client_experiment) self.setCurrentDataset(None) self.updateList() def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) self.tablecolwidth0 = settings.value('tablecolwidth0', 0, int) self.tablecolwidth1 = settings.value('tablecolwidth1', 0, int) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('tablecolwidth0', self.metaTable.columnWidth(0)) settings.setValue('tablecolwidth1', self.metaTable.columnWidth(1)) def setCustomStyle(self, font, back): self.user_font = font self.user_color = back for plot in self.setplots.values(): plot.setBackgroundColor(back) plot.update() bold = QFont(font) bold.setBold(True) larger = scaledFont(font, 1.6) for plot in self.setplots.values(): plot.setFonts(font, bold, larger) def requestClose(self): # Always succeeds, but break up circular references so that the panel # object can be deleted properly. self.currentPlot = None self.setplots.clear() return True def _autoscale(self, x=None, y=None): xflag = x if x is not None else self.actionScaleX.isChecked() yflag = y if y is not None else self.actionScaleY.isChecked() if self.currentPlot: self.currentPlot.setAutoScaleFlags(xflag, yflag) self.actionAutoScale.setChecked(xflag or yflag) self.actionScaleX.setChecked(xflag) self.actionScaleY.setChecked(yflag) self.currentPlot.update() def enablePlotActions(self, on): for action in [ self.actionSavePlot, self.actionPrint, self.actionResetPlot, self.actionAttachElog, self.actionCombine, self.actionClosePlot, self.actionDeletePlot, self.actionLogXScale, self.actionLogScale, self.actionAutoScale, self.actionScaleX, self.actionScaleY, self.actionXAxis, self.actionYAxis, self.actionNormalized, self.actionUnzoom, self.actionLegend, self.actionModifyData, self.actionFitPeak, self.actionFitPeakPV, self.actionFitPeakPVII, self.actionFitTc, self.actionFitCosine, self.actionFitSigmoid, self.actionFitArby, self.actionErrors, ]: action.setEnabled(on) def enableAutoScaleActions(self, on): for action in [self.actionAutoScale, self.actionScaleX, self.actionScaleY]: action.setEnabled(on) def getMenus(self): if not self.menus: menu1 = QMenu('&Data plot', self) menu1.addAction(self.actionSavePlot) menu1.addAction(self.actionPrint) menu1.addAction(self.actionAttachElog) menu1.addSeparator() menu1.addAction(self.actionResetPlot) menu1.addAction(self.actionAutoDisplay) menu1.addAction(self.actionCombine) menu1.addAction(self.actionClosePlot) menu1.addAction(self.actionDeletePlot) menu1.addSeparator() menu1.addAction(self.actionXAxis) menu1.addAction(self.actionYAxis) menu1.addAction(self.actionNormalized) menu1.addSeparator() menu1.addAction(self.actionUnzoom) menu1.addAction(self.actionLogXScale) menu1.addAction(self.actionLogScale) menu1.addAction(self.actionAutoScale) menu1.addAction(self.actionScaleX) menu1.addAction(self.actionScaleY) menu1.addAction(self.actionLegend) menu1.addAction(self.actionErrors) menu1.addSeparator() menu2 = QMenu('Data &manipulation', self) menu2.addAction(self.actionModifyData) menu2.addSeparator() ag = QActionGroup(menu2) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) menu2.addAction(self.actionFitPeak) menu2.addAction(self.actionPickInitial) menu2.addAction(self.actionFitPeakGaussian) menu2.addAction(self.actionFitPeakLorentzian) menu2.addAction(self.actionFitPeakPV) menu2.addAction(self.actionFitPeakPVII) menu2.addAction(self.actionFitTc) menu2.addAction(self.actionFitCosine) menu2.addAction(self.actionFitSigmoid) menu2.addAction(self.actionFitLinear) menu2.addAction(self.actionFitExponential) menu2.addSeparator() menu2.addAction(self.actionFitArby) self.menus = [menu1, menu2] return self.menus def getToolbars(self): if not self.bars: bar = QToolBar('Scans') bar.addAction(self.actionSavePlot) bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionXAxis) bar.addAction(self.actionYAxis) bar.addAction(self.actionNormalized) bar.addSeparator() bar.addAction(self.actionLogXScale) bar.addAction(self.actionLogScale) bar.addAction(self.actionUnzoom) bar.addSeparator() bar.addAction(self.actionAutoScale) bar.addAction(self.actionScaleX) bar.addAction(self.actionScaleY) bar.addAction(self.actionLegend) bar.addAction(self.actionErrors) bar.addAction(self.actionResetPlot) bar.addAction(self.actionDeletePlot) bar.addSeparator() bar.addAction(self.actionAutoDisplay) bar.addAction(self.actionCombine) fitbar = QToolBar('Scan fitting') fitbar.addAction(self.actionFitPeak) wa = QWidgetAction(fitbar) self.fitPickCheckbox = QCheckBox(fitbar) self.fitPickCheckbox.setText('Pick') self.fitPickCheckbox.setChecked(True) self.actionPickInitial.setChecked(True) self.fitPickCheckbox.toggled.connect(self.actionPickInitial.setChecked) self.actionPickInitial.toggled.connect(self.fitPickCheckbox.setChecked) layout = QHBoxLayout() layout.setContentsMargins(10, 0, 10, 0) layout.addWidget(self.fitPickCheckbox) frame = QFrame(fitbar) frame.setLayout(layout) wa.setDefaultWidget(frame) fitbar.addAction(wa) ag = QActionGroup(fitbar) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) wa = QWidgetAction(fitbar) self.fitComboBox = QComboBox(fitbar) for a in ag.actions(): itemtext = a.text().replace('&', '') self.fitComboBox.addItem(itemtext) self.fitfuncmap[itemtext] = a self.fitComboBox.currentIndexChanged.connect( self.on_fitComboBox_currentIndexChanged) wa.setDefaultWidget(self.fitComboBox) fitbar.addAction(wa) fitbar.addSeparator() fitbar.addAction(self.actionFitArby) self.bars = [bar, fitbar] return self.bars def updateList(self): self.datasetList.clear() for dataset in self.data.sets: if dataset.invisible: continue shortname = '%s - %s' % (dataset.name, dataset.default_xname) item = QListWidgetItem(shortname, self.datasetList) item.setData(32, dataset.uid) self.setitems[dataset.uid] = item def on_logYinDomain(self, flag): if not flag: self.actionLogScale.setChecked(flag) def on_logXinDomain(self, flag): if not flag: self.actionLogXScale.setChecked(flag) def on_datasetList_currentItemChanged(self, item, previous): if self.no_openset or item is None: return self.openDataset(itemuid(item)) def on_datasetList_itemClicked(self, item): # this handler is needed in addition to currentItemChanged # since one can't change the current item if it's the only one if self.no_openset or item is None: return self.openDataset(itemuid(item)) def openDataset(self, uid): dataset = self.data.uid2set[uid] newplot = None if dataset.uid not in self.setplots: newplot = DataSetPlot(self.plotFrame, self, dataset) if self.currentPlot: newplot.enableCurvesFrom(self.currentPlot) self.setplots[dataset.uid] = newplot self.datasetList.setCurrentItem(self.setitems[uid]) plot = self.setplots[dataset.uid] self.enableAutoScaleActions(plot.HAS_AUTOSCALE) if newplot and plot.HAS_AUTOSCALE: from gr.pygr import PlotAxes plot.plot.autoscale = PlotAxes.SCALE_X | PlotAxes.SCALE_Y self.setCurrentDataset(plot) def setCurrentDataset(self, plot): if self.currentPlot: self.plotLayout.removeWidget(self.currentPlot) self.currentPlot.hide() self.metaTable.clearContents() self.currentPlot = plot if plot is None: self.enablePlotActions(False) else: try: self.setUidStack.remove(plot.dataset.uid) except ValueError: pass self.setUidStack.append(plot.dataset.uid) num_items = 0 for catname in INTERESTING_CATS: if catname in plot.dataset.headerinfo: num_items += 2 + len(plot.dataset.headerinfo[catname]) num_items -= 1 # remove last empty row self.metaTable.setRowCount(num_items) i = 0 for catname in INTERESTING_CATS: if catname in plot.dataset.headerinfo: values = plot.dataset.headerinfo[catname] catdesc = catname for name_desc in INFO_CATEGORIES: if name_desc[0] == catname: catdesc = name_desc[1] catitem = QTableWidgetItem(catdesc) font = catitem.font() font.setBold(True) catitem.setFont(font) self.metaTable.setItem(i, 0, catitem) self.metaTable.setSpan(i, 0, 1, 2) i += 1 for dev, name, value in sorted(values): key = '%s_%s' % (dev, name) if name != 'value' else dev self.metaTable.setItem(i, 0, QTableWidgetItem(key)) self.metaTable.setItem(i, 1, QTableWidgetItem(value)) if self.metaTable.columnSpan(i, 0) == 2: self.metaTable.setSpan(i, 0, 1, 1) i += 1 i += 1 self.metaTable.resizeRowsToContents() self.enablePlotActions(True) self.enableAutoScaleActions(self.currentPlot.HAS_AUTOSCALE) self.datasetList.setCurrentItem(self.setitems[plot.dataset.uid]) self.actionXAxis.setText('X axis: %s' % plot.current_xname) self.actionNormalized.setChecked(bool(plot.normalized)) self.actionLogScale.setChecked(plot.isLogScaling()) self.actionLogXScale.setChecked(plot.isLogXScaling()) self.actionLegend.setChecked(plot.isLegendEnabled()) self.actionErrors.setChecked(plot.isErrorBarEnabled()) if plot.HAS_AUTOSCALE: from gr.pygr import PlotAxes mask = plot.plot.autoscale self._autoscale(x=mask & PlotAxes.SCALE_X, y=mask & PlotAxes.SCALE_Y) plot.logYinDomain.connect(self.on_logYinDomain) plot.logXinDomain.connect(self.on_logXinDomain) self.plotLayout.addWidget(plot) plot.show() def on_data_datasetAdded(self, dataset): shortname = '%s - %s' % (dataset.name, dataset.default_xname) if dataset.uid in self.setitems: self.setitems[dataset.uid].setText(shortname) if dataset.uid in self.setplots: self.setplots[dataset.uid].updateDisplay() else: self.no_openset = True item = QListWidgetItem(shortname, self.datasetList) item.setData(32, dataset.uid) self.setitems[dataset.uid] = item if self.actionAutoDisplay.isChecked() and not self.data.bulk_adding: self.openDataset(dataset.uid) self.no_openset = False # If the dataset is a continuation of another dataset, automatically # create a combined dataset. contuids = dataset.continuation if contuids: alluids = tuple(contuids.split(',')) + (dataset.uid,) # Did we already create this set? Then don't create it again. if self.contSetUids.get(alluids) in self.setitems: return allsets = list(map(self.data.uid2set.get, alluids)) newuid = self._combine(COMBINE, allsets) if newuid: self.contSetUids[alluids] = newuid def on_data_pointsAdded(self, dataset): if dataset.uid in self.setplots: self.setplots[dataset.uid].pointsAdded() def on_data_fitAdded(self, dataset, res): if dataset.uid in self.setplots: self.setplots[dataset.uid]._plotFit(res) def on_client_experiment(self, data): self.datasetList.clear() # hide plot self.setCurrentDataset(None) # back to the beginning self.setplots = {} self.setitems = {} self.currentPlot = None self.setUidStack = [] @pyqtSlot() def on_actionClosePlot_triggered(self): current_set = self.setUidStack.pop() if self.setUidStack: self.setCurrentDataset(self.setplots[self.setUidStack[-1]]) else: self.setCurrentDataset(None) del self.setplots[current_set] @pyqtSlot() def on_actionResetPlot_triggered(self): current_set = self.setUidStack.pop() del self.setplots[current_set] self.openDataset(current_set) @pyqtSlot() def on_actionDeletePlot_triggered(self): if self.currentPlot.dataset.scaninfo != 'combined set': if not self.askQuestion('This is not a combined set: still ' 'delete it from the list?'): return current_set = self.setUidStack.pop() self.data.uid2set[current_set].invisible = True if self.setUidStack: self.setCurrentDataset(self.setplots[self.setUidStack[-1]]) else: self.setCurrentDataset(None) del self.setplots[current_set] for i in range(self.datasetList.count()): if itemuid(self.datasetList.item(i)) == current_set: self.datasetList.takeItem(i) break @pyqtSlot() def on_actionSavePlot_triggered(self): filename = self.currentPlot.savePlot() if filename: self.statusBar.showMessage('Plot successfully saved to %s.' % filename) @pyqtSlot() def on_actionPrint_triggered(self): if self.currentPlot.printPlot(): self.statusBar.showMessage('Plot successfully printed.') @pyqtSlot() def on_actionAttachElog_triggered(self): newdlg = dialogFromUi(self, 'panels/plot_attach.ui') suffix = self.currentPlot.SAVE_EXT newdlg.filename.setText( safeName('data_%s' % self.currentPlot.dataset.name + suffix)) ret = newdlg.exec_() if ret != QDialog.Accepted: return descr = newdlg.description.text() fname = newdlg.filename.text() pathname = self.currentPlot.saveQuietly() with open(pathname, 'rb') as fp: remotefn = self.client.ask('transfer', fp.read()) if remotefn is not None: self.client.eval('_LogAttach(%r, [%r], [%r])' % (descr, remotefn, fname)) os.unlink(pathname) @pyqtSlot() def on_actionUnzoom_triggered(self): self.currentPlot.unzoom() @pyqtSlot(bool) def on_actionLogScale_toggled(self, on): self.currentPlot.setLogScale(on) @pyqtSlot(bool) def on_actionLogXScale_toggled(self, on): self.currentPlot.setLogXScale(on) @pyqtSlot(bool) def on_actionAutoScale_toggled(self, on): self._autoscale(on, on) @pyqtSlot(bool) def on_actionScaleX_toggled(self, on): self._autoscale(x=on) @pyqtSlot(bool) def on_actionScaleY_toggled(self, on): self._autoscale(y=on) def on_x_menu_aboutToShow(self): self.x_menu.clear() if not self.currentPlot: return done = set() for name in self.currentPlot.dataset.xnameunits: if name in done: continue done.add(name) action = self.x_menu.addAction(name) action.setData(name) action.setCheckable(True) if name == self.currentPlot.current_xname: action.setChecked(True) action.triggered.connect(self.on_x_action_triggered) @pyqtSlot() def on_x_action_triggered(self, text=None): if text is None: text = self.sender().data() self.actionXAxis.setText('X axis: %s' % text) self.currentPlot.current_xname = text self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot() def on_actionXAxis_triggered(self): self.bars[0].widgetForAction(self.actionXAxis).showMenu() def on_y_menu_aboutToShow(self): self.y_menu.clear() if not self.currentPlot: return for curve in self.currentPlot.dataset.curves: action = self.y_menu.addAction(curve.full_description) action.setData(curve.full_description) action.setCheckable(True) if not curve.hidden: action.setChecked(True) action.triggered.connect(self.on_y_action_triggered) @pyqtSlot() def on_y_action_triggered(self, text=None): if text is None: text = self.sender().data() if not self.currentPlot: return for curve in self.currentPlot.dataset.curves: if curve.full_description == text: curve.hidden = not curve.hidden self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot() def on_actionYAxis_triggered(self): self.bars[0].widgetForAction(self.actionYAxis).showMenu() @pyqtSlot() def on_actionNormalized_triggered(self): if not self.currentPlot: return if self.currentPlot.normalized is not None: self.on_norm_action_triggered('None') else: all_normnames = [name for (_, name) in self.currentPlot.dataset.normindices] if self.last_norm_selection and \ self.last_norm_selection in all_normnames: use = self.last_norm_selection else: use = all_normnames[0] if all_normnames else 'None' self.on_norm_action_triggered(use) def on_norm_menu_aboutToShow(self): self.norm_menu.clear() if self.currentPlot: none_action = self.norm_menu.addAction('None') none_action.setData('None') none_action.setCheckable(True) none_action.setChecked(True) none_action.triggered.connect(self.on_norm_action_triggered) max_action = self.norm_menu.addAction('Maximum') max_action.setData('Maximum') max_action.setCheckable(True) if self.currentPlot.normalized == 'Maximum': max_action.setChecked(True) none_action.setChecked(False) max_action.triggered.connect(self.on_norm_action_triggered) for _, name in self.currentPlot.dataset.normindices: action = self.norm_menu.addAction(name) action.setData(name) action.setCheckable(True) if name == self.currentPlot.normalized: action.setChecked(True) none_action.setChecked(False) action.triggered.connect(self.on_norm_action_triggered) @pyqtSlot() def on_norm_action_triggered(self, text=None): if text is None: text = self.sender().data() if text == 'None': self.currentPlot.normalized = None self.actionNormalized.setChecked(False) else: self.last_norm_selection = text self.currentPlot.normalized = text self.actionNormalized.setChecked(True) self.currentPlot.updateDisplay() self.on_actionUnzoom_triggered() @pyqtSlot(bool) def on_actionLegend_toggled(self, on): self.currentPlot.setLegend(on) @pyqtSlot(bool) def on_actionErrors_toggled(self, on): self.currentPlot.setErrorBarEnabled(on) @pyqtSlot() def on_actionModifyData_triggered(self): self.currentPlot.modifyData() @pyqtSlot() def on_actionFitPeak_triggered(self): self.currentPlot.beginFit(self.fitclass, self.actionFitPeak, pickmode=self.fitPickCheckbox.isChecked()) @pyqtSlot(int) def on_fitComboBox_currentIndexChanged(self, index): self.fitfuncmap[self.fitComboBox.currentText()].trigger() @pyqtSlot() def on_actionFitPeakGaussian_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakGaussian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = GaussFitter @pyqtSlot() def on_actionFitPeakLorentzian_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakLorentzian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LorentzFitter @pyqtSlot() def on_actionFitPeakPV_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPV.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PseudoVoigtFitter @pyqtSlot() def on_actionFitPeakPVII_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPVII.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PearsonVIIFitter @pyqtSlot() def on_actionFitTc_triggered(self): cbi = self.fitComboBox.findText(self.actionFitTc.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = TcFitter @pyqtSlot() def on_actionFitCosine_triggered(self): cbi = self.fitComboBox.findText(self.actionFitCosine.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = CosineFitter @pyqtSlot() def on_actionFitSigmoid_triggered(self): cbi = self.fitComboBox.findText(self.actionFitSigmoid.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = SigmoidFitter @pyqtSlot() def on_actionFitLinear_triggered(self): cbi = self.fitComboBox.findText(self.actionFitLinear.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LinearFitter @pyqtSlot() def on_actionFitExponential_triggered(self): cbi = self.fitComboBox.findText(self.actionFitExponential.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = ExponentialFitter @pyqtSlot() def on_actionFitArby_triggered(self): # no second argument: the "arbitrary" action is not checkable self.currentPlot.beginFit(ArbitraryFitter, None, pickmode=self.fitPickCheckbox.isChecked()) @pyqtSlot() def on_quickfit(self): if not self.currentPlot or not self.currentPlot.underMouse(): return self.currentPlot.fitQuick() @pyqtSlot() def on_actionCombine_triggered(self): current = self.currentPlot.dataset.uid dlg = dialogFromUi(self, 'panels/dataops.ui') for i in range(self.datasetList.count()): item = self.datasetList.item(i) newitem = QListWidgetItem(item.text(), dlg.otherList) newitem.setData(32, item.data(32)) if itemuid(item) == current: dlg.otherList.setCurrentItem(newitem) # paint the current set in grey to indicate it's not allowed # to be selected newitem.setBackground(self.palette().brush(QPalette.Mid)) newitem.setFlags(Qt.NoItemFlags) if dlg.exec_() != QDialog.Accepted: return items = dlg.otherList.selectedItems() sets = [self.data.uid2set[current]] for item in items: if itemuid(item) == current: return self.showError('Cannot combine set with itself.') sets.append(self.data.uid2set[itemuid(item)]) for rop, rb in [(TOGETHER, dlg.opTogether), (COMBINE, dlg.opCombine), (ADD, dlg.opAdd), (SUBTRACT, dlg.opSubtract), (DIVIDE, dlg.opDivide)]: if rb.isChecked(): op = rop break self._combine(op, sets) def _combine(self, op, sets): if op == TOGETHER: newset = ScanData() newset.name = combineattr(sets, 'name', sep=', ') newset.invisible = False newset.curves = [] newset.scaninfo = 'combined set' # combine xnameunits from those that are in all sets all_xnu = set(sets[0].xnameunits) for dset in sets[1:]: all_xnu &= set(dset.xnameunits) newset.xnameunits = ['Default'] + [xnu for xnu in sets[0].xnameunits if xnu in all_xnu] newset.default_xname = 'Default' newset.normindices = sets[0].normindices # for together only, the number of curves and their columns # are irrelevant, just put all together for dataset in sets: for curve in dataset.curves: newcurve = curve.copy() if not newcurve.source: newcurve.source = dataset.name newset.curves.append(newcurve) self.data.add_existing_dataset(newset, [dataset.uid for dataset in sets]) return newset.uid # else, need same axes, and same number and types of curves firstset = sets[0] nameprops = [firstset.xnames, firstset.xunits] curveprops = [(curve.description, curve.yindex) for curve in firstset.curves] for dataset in sets[1:]: if [dataset.xnames, dataset.xunits] != nameprops: self.showError('Sets have different axes.') return if [(curve.description, curve.yindex) for curve in dataset.curves] != curveprops: self.showError('Sets have different curves.') return if op == COMBINE: newset = ScanData() newset.name = combineattr(sets, 'name', sep=', ') newset.invisible = False newset.curves = [] newset.scaninfo = 'combined set' newset.xnameunits = firstset.xnameunits newset.default_xname = firstset.default_xname newset.normindices = firstset.normindices for curves in zip(*(dataset.curves for dataset in sets)): newcurve = curves[0].copy() newcurve.datay = DataProxy(c.datay for c in curves) newcurve.datady = DataProxy(c.datady for c in curves) newcurve.datax = {xnu: DataProxy(c.datax[xnu] for c in curves) for xnu in newset.xnameunits} newcurve.datanorm = {nn: DataProxy(c.datanorm[nn] for c in curves) for i, nn in newset.normindices} newset.curves.append(newcurve) self.data.add_existing_dataset(newset, [dataset.uid for dataset in sets]) return newset.uid if op == ADD: sep = ' + ' elif op == SUBTRACT: sep = ' - ' elif op == DIVIDE: sep = ' / ' newset = ScanData() newset.name = combineattr(sets, 'name', sep=sep) newset.invisible = False newset.scaninfo = 'combined set' newset.curves = [] newset.xnameunits = firstset.xnameunits newset.default_xname = firstset.default_xname if op in (SUBTRACT, DIVIDE): # remove information about normalization -- doesn't make sense newset.normindices = [] else: newset.normindices = firstset.normindices for curves in zip(*(dataset.curves for dataset in sets)): newcurve = curves[0].deepcopy() # CRUDE HACK: don't care about the x values, operate by index removepoints = set() for curve in curves[1:]: for i in range(len(newcurve.datay)): y1, y2 = float(newcurve.datay[i]), float(curve.datay[i]) if newcurve.dyindex != -1: dy1 = newcurve.datady[i] dy2 = curve.datady[i] else: dy1 = dy2 = 1. if op == ADD: newcurve.datay[i] = y1 + y2 newcurve.datady[i] = sqrt(dy1**2 + dy2**2) for name in newcurve.datanorm: newcurve.datanorm[name][i] += curve.datanorm[name][i] elif op == SUBTRACT: newcurve.datay[i] = y1 - y2 newcurve.datady[i] = sqrt(dy1**2 + dy2**2) elif op == DIVIDE: if y2 == 0: y2 = 1. # generate a value for now removepoints.add(i) newcurve.datay[i] = y1 / y2 newcurve.datady[i] = sqrt((dy1/y2)**2 + (dy2*y1 / y2**2)**2) # remove points where we would have divided by zero if removepoints: newcurve.datay = [v for (i, v) in enumerate(newcurve.datay) if i not in removepoints] newcurve.datady = [v for (i, v) in enumerate(newcurve.datady) if i not in removepoints] for name in newcurve.datax: newcurve.datax[name] = \ [v for (i, v) in enumerate(newcurve.datax[name]) if i not in removepoints] for name in newcurve.datanorm: newcurve.datanorm[name] = \ [v for (i, v) in enumerate(newcurve.datanorm[name]) if i not in removepoints] newset.curves.append(newcurve) self.data.add_existing_dataset(newset) return newset.uid
class BaseHistoryWindow(object): client = None presetdict = None def __init__(self): loadUi(self, 'panels/history.ui') self.user_color = Qt.white self.user_font = QFont('Monospace') self.views = [] # stack of views to display self.viewStack = [] # maps watched keys to their views self.keyviews = {} # current plot object self.currentPlot = None self.fitclass = LinearFitter self.fitfuncmap = {} self.enablePlotActions(False) self.presetmenu = QMenu('&Presets', self) for (name, view) in self.last_views: item = QListWidgetItem(name, self.viewList) item.setForeground(QBrush(QColor('#aaaaaa'))) item.setData(Qt.UserRole, view) self.menus = None self.bar = None # NOTE: for this class, automatic connections don't work on PyQt4 >= # 4.12 since this class is not derived from QObject. But on older PyQt4 # and PyQt5, they do work, so we change use the usual naming scheme # slightly to avoid double connections. self.viewList.currentItemChanged.connect( self.on__viewList_currentItemChanged) self.viewList.itemClicked.connect(self.on__viewList_itemClicked) self.viewList.itemDoubleClicked.connect( self.on__viewList_itemDoubleClicked) self.actionNew.triggered.connect(self.on__actionNew_triggered) self.actionEditView.triggered.connect( self.on__actionEditView_triggered) self.actionCloseView.triggered.connect( self.on__actionCloseView_triggered) self.actionResetView.triggered.connect( self.on__actionResetView_triggered) self.actionDeleteView.triggered.connect( self.on__actionDeleteView_triggered) self.actionSavePlot.triggered.connect( self.on__actionSavePlot_triggered) self.actionPrint.triggered.connect(self.on__actionPrint_triggered) self.actionUnzoom.triggered.connect(self.on__actionUnzoom_triggered) self.actionLogScale.toggled.connect(self.on__actionLogScale_toggled) self.actionAutoScale.toggled.connect(self.on__actionAutoScale_toggled) self.actionScaleX.toggled.connect(self.on__actionScaleX_toggled) self.actionScaleY.toggled.connect(self.on__actionScaleY_toggled) self.actionLegend.toggled.connect(self.on__actionLegend_toggled) self.actionSymbols.toggled.connect(self.on__actionSymbols_toggled) self.actionLines.toggled.connect(self.on__actionLines_toggled) self.actionSaveData.triggered.connect( self.on__actionSaveData_triggered) self.actionFitPeak.triggered.connect(self.on__actionFitPeak_triggered) self.actionFitArby.triggered.connect(self.on__actionFitArby_triggered) self.actionFitPeakGaussian.triggered.connect( self.on__actionFitPeakGaussian_triggered) self.actionFitPeakLorentzian.triggered.connect( self.on__actionFitPeakLorentzian_triggered) self.actionFitPeakPV.triggered.connect( self.on__actionFitPeakPV_triggered) self.actionFitPeakPVII.triggered.connect( self.on__actionFitPeakPVII_triggered) self.actionFitTc.triggered.connect(self.on__actionFitTc_triggered) self.actionFitCosine.triggered.connect( self.on__actionFitCosine_triggered) self.actionFitSigmoid.triggered.connect( self.on__actionFitSigmoid_triggered) self.actionFitLinear.triggered.connect( self.on__actionFitLinear_triggered) self.actionFitExponential.triggered.connect( self.on__actionFitExponential_triggered) def getMenus(self): menu = QMenu('&History viewer', self) menu.addAction(self.actionNew) menu.addSeparator() menu.addAction(self.actionSavePlot) menu.addAction(self.actionPrint) menu.addAction(self.actionAttachElog) menu.addAction(self.actionSaveData) menu.addSeparator() menu.addAction(self.actionEditView) menu.addAction(self.actionCloseView) menu.addAction(self.actionDeleteView) menu.addAction(self.actionResetView) menu.addSeparator() menu.addAction(self.actionLogScale) menu.addAction(self.actionAutoScale) menu.addAction(self.actionScaleX) menu.addAction(self.actionScaleY) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLegend) menu.addAction(self.actionSymbols) menu.addAction(self.actionLines) ag = QActionGroup(menu) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) menu.addAction(self.actionFitPeak) menu.addAction(self.actionPickInitial) menu.addAction(self.actionFitPeakGaussian) menu.addAction(self.actionFitPeakLorentzian) menu.addAction(self.actionFitPeakPV) menu.addAction(self.actionFitPeakPVII) menu.addAction(self.actionFitTc) menu.addAction(self.actionFitCosine) menu.addAction(self.actionFitSigmoid) menu.addAction(self.actionFitLinear) menu.addAction(self.actionFitExponential) menu.addSeparator() menu.addAction(self.actionFitArby) menu.addSeparator() menu.addAction(self.actionClose) self._refresh_presets() return [menu, self.presetmenu] def getToolbars(self): if not self.bar: bar = QToolBar('History viewer') bar.addAction(self.actionNew) bar.addAction(self.actionEditView) bar.addSeparator() bar.addAction(self.actionSavePlot) bar.addAction(self.actionPrint) bar.addAction(self.actionSaveData) bar.addSeparator() bar.addAction(self.actionUnzoom) bar.addAction(self.actionLogScale) bar.addSeparator() bar.addAction(self.actionAutoScale) bar.addAction(self.actionScaleX) bar.addAction(self.actionScaleY) bar.addSeparator() bar.addAction(self.actionResetView) bar.addAction(self.actionDeleteView) bar.addSeparator() bar.addAction(self.actionFitPeak) wa = QWidgetAction(bar) self.fitPickCheckbox = QCheckBox(bar) self.fitPickCheckbox.setText('Pick') self.fitPickCheckbox.setChecked(True) self.actionPickInitial.setChecked(True) self.fitPickCheckbox.toggled.connect( self.actionPickInitial.setChecked) self.actionPickInitial.toggled.connect( self.fitPickCheckbox.setChecked) layout = QHBoxLayout() layout.setContentsMargins(10, 0, 10, 0) layout.addWidget(self.fitPickCheckbox) frame = QFrame(bar) frame.setLayout(layout) wa.setDefaultWidget(frame) bar.addAction(wa) ag = QActionGroup(bar) ag.addAction(self.actionFitPeakGaussian) ag.addAction(self.actionFitPeakLorentzian) ag.addAction(self.actionFitPeakPV) ag.addAction(self.actionFitPeakPVII) ag.addAction(self.actionFitTc) ag.addAction(self.actionFitCosine) ag.addAction(self.actionFitSigmoid) ag.addAction(self.actionFitLinear) ag.addAction(self.actionFitExponential) wa = QWidgetAction(bar) self.fitComboBox = QComboBox(bar) for a in ag.actions(): itemtext = a.text().replace('&', '') self.fitComboBox.addItem(itemtext) self.fitfuncmap[itemtext] = a self.fitComboBox.currentIndexChanged.connect( self.on__fitComboBox_currentIndexChanged) wa.setDefaultWidget(self.fitComboBox) bar.addAction(wa) bar.addSeparator() bar.addAction(self.actionFitArby) self.bar = bar self.actionFitLinear.trigger() return [self.bar] def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) self.presetdict = {} # read new format if present settings.beginGroup('presets_new') for key in settings.childKeys(): self.presetdict[key] = json.loads(settings.value(key)) settings.endGroup() # convert old format try: presetval = settings.value('presets') if presetval: for (name, value) in presetval.items(): if not isinstance(value, bytes): value = value.encode('latin1') self.presetdict[name] = pickle.loads(value) except Exception: pass settings.remove('presets') self.last_views = [] settings.beginGroup('views_new') for key in settings.childKeys(): try: info = json.loads(settings.value(key)) self.last_views.append((key, info)) except Exception: pass settings.endGroup() def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.beginGroup('presets_new') for (key, info) in self.presetdict.items(): settings.setValue(key, json.dumps(info)) settings.endGroup() settings.beginGroup('views_new') for view in self.views: settings.setValue(view.name, json.dumps(view.dlginfo)) settings.endGroup() def openViews(self, views): """Open some views given by the specs in *views*, a list of strings. Each string can be a comma-separated list of key names, and an optional simple time spec (like "1h") separated by a colon. If a view spec matches the name of a preset, it is used instead. """ for viewspec in views: timespec = '1h' if ':' in viewspec: viewspec, timespec = viewspec.rsplit(':', 1) info = dict( name=viewspec, devices=viewspec, simpleTime=True, simpleTimeSpec=timespec, slidingWindow=True, frombox=False, tobox=False, fromdate=0, todate=0, interval='', customY=False, customYFrom='', customYTo='', ) self._createViewFromDialog(info) def _refresh_presets(self): pmenu = self.presetmenu pmenu.clear() delmenu = QMenu('Delete', self) try: for preset, info in iteritems(self.presetdict): paction = QAction(preset, self) pdelaction = QAction(preset, self) info = info.copy() def launchpreset(on, info=info): self._createViewFromDialog(info) def delpreset(on, name=preset, act=paction, delact=pdelaction): pmenu.removeAction(act) delmenu.removeAction(delact) self.presetdict.pop(name, None) self._refresh_presets() paction.triggered[bool].connect(launchpreset) pmenu.addAction(paction) pdelaction.triggered[bool].connect(delpreset) delmenu.addAction(pdelaction) except AttributeError: self.presetdict = {} if self.presetdict: pmenu.addSeparator() pmenu.addMenu(delmenu) else: pmenu.addAction('(no presets created)') def _add_preset(self, name, info): if name: self.presetdict[name] = info.copy() self._refresh_presets() def _autoscale(self, x=None, y=None): xflag = x if x is not None else self.actionScaleX.isChecked() yflag = y if y is not None else self.actionScaleY.isChecked() if self.currentPlot: self.currentPlot.setAutoScaleFlags(xflag, yflag) self.actionAutoScale.setChecked(xflag or yflag) self.actionScaleX.setChecked(xflag) self.actionScaleY.setChecked(yflag) self.currentPlot.update() def enablePlotActions(self, on): for action in [ self.actionSavePlot, self.actionPrint, self.actionAttachElog, self.actionSaveData, self.actionAutoScale, self.actionScaleX, self.actionScaleY, self.actionEditView, self.actionCloseView, self.actionDeleteView, self.actionResetView, self.actionUnzoom, self.actionLogScale, self.actionLegend, self.actionSymbols, self.actionLines, self.actionFitPeak, self.actionFitArby, ]: action.setEnabled(on) def enableAutoScaleActions(self, on): for action in [ self.actionAutoScale, self.actionScaleX, self.actionScaleY ]: action.setEnabled(on) def on__fitComboBox_currentIndexChanged(self, index): self.fitfuncmap[self.fitComboBox.currentText()].trigger() def on__viewList_currentItemChanged(self, item, previous): if item is None: return for view in self.views: if view.listitem == item: self.openView(view) def on__viewList_itemClicked(self, item): # this handler is needed in addition to currentItemChanged # since one can't change the current item if it's the only one self.on__viewList_currentItemChanged(item, None) # is it a "saved from last time" item? info = item.data(Qt.UserRole) if info is not None: row = self.viewList.row(item) do_restore = self.askQuestion('Restore this view from last time?') self.viewList.takeItem(row) if do_restore: self._createViewFromDialog(info, row) def on_logYinDomain(self, flag): if not flag: self.actionLogScale.setChecked(flag) def newvalue_callback(self, data): (vtime, key, op, value) = data if key not in self.keyviews: return if not value: return value = cache_load(value) for view in self.keyviews[key]: view.newValue(vtime, key, op, value) def _createViewFromDialog(self, info, row=None): if not info['devices'].strip(): return keys_indices = [ extractKeyAndIndex(d.strip()) for d in info['devices'].split(',') ] if self.client is not None: meta = self._getMetainfo(keys_indices) else: meta = ({}, {}) name = info['name'] if not name: name = info['devices'] if info['simpleTime']: name += ' (%s)' % info['simpleTimeSpec'] window = None if info['simpleTime']: try: itime, _ = get_time_and_interval(info['simpleTimeSpec']) except ValueError: return fromtime = currenttime() - itime totime = None if info['slidingWindow']: window = itime else: if info['frombox']: fromtime = mktime(localtime(info['fromdate'])) else: fromtime = None if info['tobox']: totime = mktime(localtime(info['todate'])) else: totime = None try: interval = float(info['interval']) except ValueError: interval = 5.0 if info['customY']: try: yfrom = float(info['customYFrom']) except ValueError: return try: yto = float(info['customYTo']) except ValueError: return else: yfrom = yto = None view = View(self, name, keys_indices, interval, fromtime, totime, yfrom, yto, window, meta, info, self.gethistory_callback) self.views.append(view) view.listitem = QListWidgetItem(view.name) if row is not None: self.viewList.insertItem(row, view.listitem) else: self.viewList.addItem(view.listitem) self.openView(view) if view.totime is None: for key in view.uniq_keys: self.keyviews.setdefault(key, []).append(view) return view def _getMetainfo(self, keys_indices): """Collect unit and string<->integer mapping for each key that refers to a device main value. """ units = {} mappings = {} seen = set() for key, _, _, _ in keys_indices: if key in seen or not key.endswith('/value'): continue seen.add(key) devname = key[:-6] devunit = self.client.getDeviceParam(devname, 'unit') if devunit: units[key] = devunit devmapping = self.client.getDeviceParam(devname, 'mapping') if devmapping: mappings[key] = m = {} i = 0 for k, v in sorted(devmapping.items()): if isinstance(v, integer_types): m[k] = v else: m[k] = i i += 1 return units, mappings def on__actionNew_triggered(self): self.showNewDialog() def showNewDialog(self, devices=''): newdlg = NewViewDialog(self, client=self.client) newdlg.devices.setText(devices) ret = newdlg.exec_() if ret != QDialog.Accepted: return info = newdlg.infoDict() self._createViewFromDialog(info) if newdlg.savePreset.isChecked(): self._add_preset(info['name'], info) def newView(self, devices): newdlg = NewViewDialog(self) newdlg.devices.setText(devices) info = newdlg.infoDict() self._createViewFromDialog(info) def openView(self, view): if not view.plot: view.plot = ViewPlot(self.plotFrame, self, view) self.viewList.setCurrentItem(view.listitem) self.setCurrentView(view) def setCurrentView(self, view): newView = False if self.currentPlot: self.plotLayout.removeWidget(self.currentPlot) self.currentPlot.hide() if view is None: self.currentPlot = None self.enablePlotActions(False) else: self.currentPlot = view.plot try: self.viewStack.remove(view) except ValueError: newView = True self.viewStack.append(view) self.enablePlotActions(True) self.enableAutoScaleActions(view.plot.HAS_AUTOSCALE) self.viewList.setCurrentItem(view.listitem) self.actionLogScale.setChecked(view.plot.isLogScaling()) self.actionLegend.setChecked(view.plot.isLegendEnabled()) self.actionSymbols.setChecked(view.plot.hasSymbols) self.actionLines.setChecked(view.plot.hasLines) self.plotLayout.addWidget(view.plot) if view.plot.HAS_AUTOSCALE: from gr.pygr import PlotAxes if newView: mask = PlotAxes.SCALE_X | PlotAxes.SCALE_Y else: mask = view.plot.plot.autoscale if view.yfrom and view.yto: mask &= ~PlotAxes.SCALE_Y self._autoscale(x=mask & PlotAxes.SCALE_X, y=mask & PlotAxes.SCALE_Y) view.plot.logYinDomain.connect(self.on_logYinDomain) view.plot.setSlidingWindow(view.window) view.plot.show() def on__viewList_itemDoubleClicked(self, item): if item: self.on__actionEditView_triggered() def on__actionEditView_triggered(self): view = self.viewStack[-1] newdlg = NewViewDialog(self, view.dlginfo, client=self.client) newdlg.setWindowTitle('Edit history view') ret = newdlg.exec_() if ret != QDialog.Accepted: return info = newdlg.infoDict() if newdlg.savePreset.isChecked(): self._add_preset(info['name'], info) self.viewStack.pop() row = self.clearView(view) new_view = self._createViewFromDialog(info, row) if new_view.plot.HAS_AUTOSCALE: self._autoscale(True, False) def on__actionCloseView_triggered(self): view = self.viewStack.pop() if self.viewStack: self.setCurrentView(self.viewStack[-1]) else: self.setCurrentView(None) view.plot = None def on__actionResetView_triggered(self): view = self.viewStack.pop() hassym = view.plot.hasSymbols view.plot = None self.openView(view) self.actionSymbols.setChecked(hassym) view.plot.setSymbols(hassym) def on__actionDeleteView_triggered(self): view = self.viewStack.pop() self.clearView(view) if self.viewStack: self.setCurrentView(self.viewStack[-1]) else: self.setCurrentView(None) def clearView(self, view): self.views.remove(view) row = self.viewList.row(view.listitem) self.viewList.takeItem(row) if view.totime is None: for key in view.uniq_keys: self.keyviews[key].remove(view) view.cleanup() return row def on__actionSavePlot_triggered(self): filename = self.currentPlot.savePlot() if filename: self.statusBar.showMessage('View successfully saved to %s.' % filename) def on__actionPrint_triggered(self): if self.currentPlot.printPlot(): self.statusBar.showMessage('View successfully printed.') def on__actionUnzoom_triggered(self): self.currentPlot.unzoom() def on__actionLogScale_toggled(self, on): self.currentPlot.setLogScale(on) def on__actionAutoScale_toggled(self, on): self._autoscale(on, on) def on__actionScaleX_toggled(self, on): self._autoscale(x=on) def on__actionScaleY_toggled(self, on): self._autoscale(y=on) def on__actionLegend_toggled(self, on): self.currentPlot.setLegend(on) def on__actionSymbols_toggled(self, on): self.currentPlot.setSymbols(on) def on__actionLines_toggled(self, on): self.currentPlot.setLines(on) def on__actionSaveData_triggered(self): self.currentPlot.saveData() def on__actionFitPeak_triggered(self): self.currentPlot.beginFit(self.fitclass, self.actionFitPeak, self.fitPickCheckbox.isChecked()) def on__actionFitLinear_triggered(self): cbi = self.fitComboBox.findText(self.actionFitLinear.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LinearFitter def on__actionFitExponential_triggered(self): cbi = self.fitComboBox.findText( self.actionFitExponential.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = ExponentialFitter def on__actionFitPeakGaussian_triggered(self): cbi = self.fitComboBox.findText( self.actionFitPeakGaussian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = GaussFitter def on__actionFitPeakLorentzian_triggered(self): cbi = self.fitComboBox.findText( self.actionFitPeakLorentzian.text().replace('&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = LorentzFitter def on__actionFitPeakPV_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPV.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PseudoVoigtFitter def on__actionFitPeakPVII_triggered(self): cbi = self.fitComboBox.findText(self.actionFitPeakPVII.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = PearsonVIIFitter def on__actionFitTc_triggered(self): cbi = self.fitComboBox.findText(self.actionFitTc.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = TcFitter def on__actionFitCosine_triggered(self): cbi = self.fitComboBox.findText(self.actionFitCosine.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = CosineFitter def on__actionFitSigmoid_triggered(self): cbi = self.fitComboBox.findText(self.actionFitSigmoid.text().replace( '&', '')) self.fitComboBox.setCurrentIndex(cbi) self.fitclass = SigmoidFitter def on__actionFitArby_triggered(self): # no second argument: the "arbitrary" action is not checkable self.currentPlot.beginFit(ArbitraryFitter, None, self.fitPickCheckbox.isChecked())