def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_mlz/mira/gui/live.ui')) self._format = None self._runtime = 0 self._no_direct_display = False self._range_active = False self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) if CascadeWidget: self.widget = CascadeWidget(self) self.widget.setContextMenuPolicy(Qt.CustomContextMenu) self.widgetLayout.addWidget(self.widget) else: raise RuntimeError('The Cascade live widget is not available') self.rangeFrom = QDoubleSpinBox(self) self.rangeTo = QDoubleSpinBox(self) for ctrl in [self.rangeFrom, self.rangeTo]: ctrl.setRange(0, 100000000) ctrl.setEnabled(False) ctrl.setMaximumWidth(90) ctrl.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) ctrl.valueChanged.connect(self.on_rangeChanged) self.liveitem = QListWidgetItem('<Live>', self.fileList) self.liveitem.setData(32, '') self.liveitem.setData(33, '') self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) if client.isconnected: self.on_client_connected() client.connected.connect(self.on_client_connected) client.setup.connect(self.on_client_connected) self.actionLogScale.toggled.connect(self.widget.SetLog10) self.actionSelectChannels.triggered.connect(self.widget.showSumDlg) self.widget.customContextMenuRequested.connect( self.on_widget_customContextMenuRequested)
def __init__(self, app): QMainWindow.__init__(self, None) self.app = app self.client = app # used by the NewViewDialog # this is done in Panel.__init__ for the panel version self.settings = CompatSettings() self.loadSettings(self.settings) BaseHistoryWindow.__init__(self) self.splitter.setSizes([20, 80]) DlgUtils.__init__(self, 'History viewer') self.actionAttachElog.setVisible(False) self.splitter.restoreState(self.splitterstate) self.setCentralWidget(self.splitter) self.newValue.connect(self.newvalue_callback) for toolbar in self.getToolbars(): self.addToolBar(toolbar) for menu in self.getMenus(): self.menuBar().addMenu(menu) self.actionFitLinear.trigger() self.statusBar = QStatusBar(self) self.setStatusBar(self.statusBar)
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) BaseHistoryWindow.__init__(self) self.actionClose.setVisible(False) self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self._disconnected_since = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) self.client.cache.connect(self.newvalue_callback) self.client.disconnected.connect(self.on_client_disconnected) self.client.connected.connect(self.on_client_connected)
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_demo/demo/gui/sanspanel.ui')) self.current_status = None self._idle = QColor('#99FF99') self._busy = QColor(Qt.yellow) self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) client.cache.connect(self.on_client_cache) run = self.buttonBox.button(QDialogButtonBox.Yes) run.setText('Run') run.setIcon(QIcon(':/continue')) self.buttonBox.accepted.connect(self.on_start_clicked)
class LiveDataPanel(Panel): panelName = 'Live data view' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_mlz/mira/gui/live.ui')) self._format = None self._runtime = 0 self._no_direct_display = False self._range_active = False self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) if CascadeWidget: self.widget = CascadeWidget(self) self.widget.setContextMenuPolicy(Qt.CustomContextMenu) self.widgetLayout.addWidget(self.widget) else: raise RuntimeError('The Cascade live widget is not available') self.rangeFrom = QDoubleSpinBox(self) self.rangeTo = QDoubleSpinBox(self) for ctrl in [self.rangeFrom, self.rangeTo]: ctrl.setRange(0, 100000000) ctrl.setEnabled(False) ctrl.setMaximumWidth(90) ctrl.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) ctrl.valueChanged.connect(self.on_rangeChanged) self.liveitem = QListWidgetItem('<Live>', self.fileList) self.liveitem.setData(32, '') self.liveitem.setData(33, '') self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) client.livedata.connect(self.on_client_livedata) if client.isconnected: self.on_client_connected() client.connected.connect(self.on_client_connected) client.setup.connect(self.on_client_connected) self.actionLogScale.toggled.connect(self.widget.SetLog10) self.actionSelectChannels.triggered.connect(self.widget.showSumDlg) self.widget.customContextMenuRequested.connect( self.on_widget_customContextMenuRequested) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) def getMenus(self): self.menu = menu = QMenu('&Live data', self) menu.addAction(self.actionLoadTOF) menu.addAction(self.actionLoadPAD) menu.addSeparator() menu.addAction(self.actionWriteXml) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionSetAsROI) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionNormalized) menu.addAction(self.actionLegend) return [menu] def getToolbars(self): bar = QToolBar('Live data') bar.addAction(self.actionWriteXml) bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionLogScale) bar.addSeparator() bar.addAction(self.actionUnzoom) bar.addAction(self.actionSetAsROI) bar.addSeparator() bar.addAction(self.actionSelectChannels) bar.addSeparator() bar.addAction(self.actionCustomRange) bar.addWidget(self.rangeFrom) bar.addWidget(QLabel(' to ')) bar.addWidget(self.rangeTo) bar.addSeparator() bar.addAction(self.actionOverviewMode) bar.addAction(self.actionPhaseMode) bar.addAction(self.actionContrastMode) return [bar] def on_widget_customContextMenuRequested(self, point): self.menu.popup(self.mapToGlobal(point)) def on_client_connected(self): self.client.tell('eventunmask', ['livedata']) datapath = self.client.eval('session.experiment.datapath', '') if not datapath: return caspath = path.join(datapath, 'cascade') if path.isdir(caspath): for fn in sorted(os.listdir(caspath)): if fn.endswith('.pad'): self.add_to_flist(path.join(caspath, fn), 'pad', False) elif fn.endswith('tof'): self.add_to_flist(path.join(caspath, fn), 'tof', False) def on_client_livedata(self, params, blobs): tag, _uid, _det, filename, dtype, nx, ny, nt, runtime = params # TODO: remove compatibility code if not isinstance(filename, str): filename, nx, ny, nt = filename[0], nx[0], ny[0], nt[0] if dtype == '<u4' and nx == 128 and ny == 128 and tag != 'MiraXML': if nt == 1: self._format = 'pad' elif nt == 128: self._format = 'tof' self._runtime = runtime self._filename = filename else: if filename and tag != 'MiraXML': self._filename = filename self._format = filename[-3:] else: # print 'Unsupported live data format:', params self._format = None for blob in blobs: self._process_livedata(blob) def _process_livedata(self, data): if self._format not in ('pad', 'tof'): return if data: self._last_data = data if not self._no_direct_display and data: runtime = self._runtime or 1e-6 if self._format == 'pad': self.widget.LoadPadMem(data, 128*128*4) cts = self.widget.GetPad().GetCounts() self.statusBar.showMessage('cps: %.2f | total: %s' % (cts/runtime, cts)) else: self.widget.LoadTofMem(data, 128*128*128*4) cts = self.widget.GetTof().GetCounts() self.statusBar.showMessage('cps: %.2f | total: %s' % (cts/runtime, cts)) self.updateRange() if self._filename and not self._filename.startswith(('live@', '<Live>@')): # and path.isfile(self._filename): if 'mira_cas' not in self._filename: self.add_to_flist(self._filename, self._format) def on_fileList_itemClicked(self, item): if item is None: return fname = item.data(32) fformat = item.data(33) if not fname: if self._no_direct_display: self._no_direct_display = False if self._format == 'pad': self.widget.LoadPadMem(self._last_data, 128*128*4) cts = self.widget.GetPad().GetCounts() self.statusBar.showMessage('total: %s' % cts) elif self._format == 'tof': self.widget.LoadTofMem(self._last_data, 128*128*128*4) cts = self.widget.GetTof().GetCounts() self.statusBar.showMessage('total: %s' % cts) self.updateRange() else: self._no_direct_display = True if fformat == 'pad': self.widget.LoadPadFile(fname) cts = self.widget.GetPad().GetCounts() self.statusBar.showMessage('total: %s' % cts) elif fformat == 'tof': self.widget.LoadTofFile(fname) cts = self.widget.GetTof().GetCounts() self.statusBar.showMessage('total: %s' % cts) self.updateRange() def on_fileList_currentItemChanged(self, item, previous): self.on_fileList_itemClicked(item) @pyqtSlot() def on_actionLoadTOF_triggered(self): filename = QFileDialog.getOpenFileName(self, 'Open TOF File', '', 'TOF File (*.tof *.TOF);;All files (*)')[0] if filename: self.widget.LoadTofFile(filename) self.add_to_flist(filename, 'tof') @pyqtSlot() def on_actionLoadPAD_triggered(self): filename = QFileDialog.getOpenFileName(self, 'Open PAD File', '', 'PAD File (*.pad *.PAD);;All files (*)')[0] if filename: self.widget.LoadPadFile(filename) self.updateRange() self.add_to_flist(filename, 'pad') def add_to_flist(self, filename, fformat, scroll=True): shortname = path.basename(filename) if self.fileList.count() > 2 and \ self.fileList.item(self.fileList.count()-2).text() == shortname: return item = QListWidgetItem(shortname) item.setData(32, filename) item.setData(33, fformat) self.fileList.insertItem(self.fileList.count()-1, item) if scroll: self.fileList.scrollToBottom() @pyqtSlot() def on_actionWriteXml_triggered(self): pad = self.widget.GetPad() if pad is None: return self.showError('No 2-d image is shown.') filename = str(QFileDialog.getSaveFileName( self, 'Select file name', '', 'XML files (*.xml)')[0]) if not filename: return if not filename.endswith('.xml'): filename += '.xml' if TmpImage: tmpimg = TmpImage() tmpimg.ConvertPAD(pad) tmpimg.WriteXML(filename) @pyqtSlot() def on_actionSetAsROI_triggered(self): zoom = self.widget.GetPlot().GetZoomer().zoomRect() self.client.run('psd_channel.roi = (%s, %s, %s, %s)' % (int(zoom.left()), int(zoom.top()), int(zoom.right()), int(zoom.bottom()))) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.GetPlot().GetZoomer().zoom(0) @pyqtSlot() def on_actionPrint_triggered(self): printer = QPrinter(QPrinter.HighResolution) printer.setColorMode(QPrinter.Color) printer.setOrientation(QPrinter.Landscape) printer.setOutputFileName('') if QPrintDialog(printer, self).exec_() == QDialog.Accepted: self.widget.GetPlot().print_(printer) self.statusBar.showMessage('Plot successfully printed to %s.' % str(printer.printerName())) def on_actionCustomRange_toggled(self, on): self.rangeFrom.setEnabled(on) self.rangeTo.setEnabled(on) self.widget.SetAutoCountRange(not on) self._range_active = on if on: self.on_rangeChanged(0) else: self.updateRange() def on_rangeChanged(self, val): if self._range_active: self.widget.SetCountRange(self.rangeFrom.value(), self.rangeTo.value()) def updateRange(self): if not self.actionCustomRange.isChecked(): crange = self.widget.GetData2d().range() self.rangeFrom.setValue(crange.minValue()) self.rangeTo.setValue(crange.maxValue()) def closeEvent(self, event): with self.sgroup as settings: settings.setValue('geometry', self.saveGeometry()) event.accept() @pyqtSlot() def on_actionOverviewMode_triggered(self): self.widget.viewOverview() @pyqtSlot() def on_actionPhaseMode_triggered(self): self.widget.SetFoil(7) self.widget.viewPhases() @pyqtSlot() def on_actionContrastMode_triggered(self): self.widget.SetFoil(7) self.widget.viewContrasts()
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()
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 HistoryPanel(BaseHistoryWindow, Panel): """Provides a panel to show time series plots of any cache values.""" panelName = 'History viewer' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) BaseHistoryWindow.__init__(self) self.actionClose.setVisible(False) self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self._disconnected_since = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) self.client.cache.connect(self.newvalue_callback) self.client.disconnected.connect(self.on_client_disconnected) self.client.connected.connect(self.on_client_connected) def setCustomStyle(self, font, back): self.user_font = font self.user_color = back for view in self.views: if view.plot: view.plot.setBackgroundColor(back) view.plot.update() bold = QFont(font) bold.setBold(True) larger = scaledFont(font, 1.6) for view in self.views: if view.plot: view.plot.setFonts(font, bold, larger) def requestClose(self): # Always succeeds, but break up circular references so that the panel # object can be deleted properly. for v in self.views: v.plot = None self.currentPlot = None self.client.cache.disconnect(self.newvalue_callback) return True def gethistory_callback(self, key, fromtime, totime): return self.client.ask('gethistory', key, str(fromtime), str(totime), default=[]) def on_client_disconnected(self): self._disconnected_since = currenttime() def on_client_connected(self): # If the client was disconnected for longer than a few seconds, refresh # all open plots to avoid mysterious "flatlines" for that period. if currenttime() - self._disconnected_since < 5: return old_views, self.viewStack = self.viewStack, [] for view in old_views: info = view.dlginfo row = self.clearView(view) new_view = self._createViewFromDialog(info, row) if new_view.plot.HAS_AUTOSCALE: self._autoscale(True, False) @pyqtSlot() def on_actionAttachElog_triggered(self): newdlg = dialogFromUi(self, 'panels/plot_attach.ui') suffix = self.currentPlot.SAVE_EXT newdlg.filename.setText( safeName('history_%s' % self.currentPlot.view.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)
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/live.ui') self._allowed_tags = set() self._allowed_detectors = set() self._ignore_livedata = False # ignore livedata, e.g. wrong detector self._last_idx = 0 self._last_tag = None self._last_fnames = None self._last_format = None self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # self.widget.setControls(Logscale | MinimumMaximum | BrightnessContrast | # Integrate | Histogram) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure instrument specific behavior self._instrument = options.get('instrument', '') # self.widget.setInstrumentOption(self._instrument) # if self._instrument == 'toftof': # self.widget.setAxisLabels('time channels', 'detectors') # elif self._instrument == 'imaging': # self.widget.setControls(ShowGrid | Logscale | Grayscale | # Normalize | Darkfield | Despeckle | # CreateProfile | Histogram | MinimumMaximum) # self.widget.setStandardColorMap(True, False) # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_tags = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize)
class LiveDataPanel(Panel): """Provides a generic "detector live view". For most instruments, a specific panel must be implemented that takes care of the individual live display needs. Options: * ``instrument`` -- the instrument name that is passed on to the livewidget module. * ``filetypes`` default[] - List of filename extensions whose content should be displayed. * ``detectors`` (default [] - list of detector devices whose data should be displayed. If not set data from all configured detectors will be shown. * ``cachesize`` (default 20) - Number of entries in the live data cache. The live data cache allows to display of previous taken data. """ panelName = 'Live data view' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/live.ui') self._allowed_tags = set() self._allowed_detectors = set() self._ignore_livedata = False # ignore livedata, e.g. wrong detector self._last_idx = 0 self._last_tag = None self._last_fnames = None self._last_format = None self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # self.widget.setControls(Logscale | MinimumMaximum | BrightnessContrast | # Integrate | Histogram) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure instrument specific behavior self._instrument = options.get('instrument', '') # self.widget.setInstrumentOption(self._instrument) # if self._instrument == 'toftof': # self.widget.setAxisLabels('time channels', 'detectors') # elif self._instrument == 'imaging': # self.widget.setControls(ShowGrid | Logscale | Grayscale | # Normalize | Darkfield | Despeckle | # CreateProfile | Histogram | MinimumMaximum) # self.widget.setStandardColorMap(True, False) # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_tags = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) def setLiveItems(self, n): nitems = len(self.liveitems) if n < nitems: nfiles = self.fileList.count() for i in range(nitems - 1, n - 1, -1): self.liveitems.pop(i) self.fileList.takeItem(nfiles - nitems + i) if self._livechannel > n: self._livechannel = 0 if n > 0 else None else: for i in range(nitems, n): item = QListWidgetItem('<Live #%d>' % (i + 1)) item.setData(FILENAME, i) item.setData(FILEFORMAT, '') item.setData(FILETAG, 'live') self.fileList.insertItem(self.fileList.count(), item) self.liveitems.append(item) if n == 1: self.liveitems[0].setText('<Live>') else: self.liveitems[0].setText('<Live #1>') def set2DControlsEnabled(self, flag): if flag != self.actionKeepRatio.isChecked(): self.actionKeepRatio.trigger() for action in self._actions2D: action.setVisible(flag) def setControlsEnabled(self, flag): for action in self.toolbar.actions(): action.setEnabled(flag) self.actionOpen.setEnabled(True) # File Open action always available def initLiveWidget(self, widgetcls): if isinstance(self.widget, widgetcls): return if self.widget: self.widgetLayout.removeWidget(self.widget) self.widget.deleteLater() self.widget = widgetcls(self) # enable/disable controls and set defaults for new livewidget instances self.setControlsEnabled(True) if isinstance(self.widget, LiveWidget1D): self.set2DControlsEnabled(False) else: self.set2DControlsEnabled(True) # apply current settings self.widget.setCenterMark(self.actionMarkCenter.isChecked()) self.widget.logscale(self.actionLogScale.isChecked()) guiConn = GUIConnector(self.widget.gr) guiConn.connect(MouseEvent.MOUSE_MOVE, self.on_mousemove_gr) self.menuColormap = QMenu(self) self.actionsColormap = QActionGroup(self) activeMap = self.widget.getColormap() activeCaption = None for name, value in iteritems(COLORMAPS): caption = name.title() action = self.menuColormap.addAction(caption) action.setData(caption) action.setCheckable(True) if activeMap == value: action.setChecked(True) # update toolButton text later otherwise this may fail # depending on the setup and qt versions in use activeCaption = caption self.actionsColormap.addAction(action) action.triggered.connect(self.on_colormap_triggered) self.actionColormap.setMenu(self.menuColormap) self.widgetLayout.addWidget(self.widget) if activeCaption: self.toolbar.widgetForAction( self.actionColormap).setText(activeCaption) detectors = self.client.eval('session.experiment.detectors', []) self._register_rois(detectors) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('geometry', self.saveGeometry()) def getMenus(self): if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) self.menu = menu return [self.menu] def _get_all_widgets(self): yield self.widget for w in itervalues(self._livewidgets): yield w def getToolbars(self): return [self.toolbar] def on_mousemove_gr(self, event): xyz = None if event.getWindow(): # inside plot xyz = self.widget.getZValue(event) if xyz: fmt = '(%g, %g)' # x, y data 1D integral plots if len(xyz) == 3: fmt += ': %g' # x, y, z data for 2D image plot self.statusBar.showMessage(fmt % xyz) else: self.statusBar.clearMessage() def on_actionColormap_triggered(self): w = self.toolbar.widgetForAction(self.actionColormap) m = self.actionColormap.menu() if m: m.popup(w.mapToGlobal(QPoint(0, w.height()))) def on_colormap_triggered(self): action = self.actionsColormap.checkedAction() name = action.data() for widget in self._get_all_widgets(): widget.setColormap(COLORMAPS[name.upper()]) self.toolbar.widgetForAction(self.actionColormap).setText(name.title()) def _getLiveWidget(self, roi): return self._livewidgets.get(roi + '/roi', None) def showRoiWindow(self, roikey): key = roikey + '/roi' widget = self._getLiveWidget(roikey) region = self.widget._rois[key] if not widget: widget = LiveWidget(None) widget.setWindowTitle(roikey) widget.setColormap(self.widget.getColormap()) widget.setCenterMark(self.actionMarkCenter.isChecked()) widget.logscale(self.actionLogScale.isChecked()) widget.gr.setAdjustSelection(False) # don't use adjust on ROIs for name, roi in iteritems(self.rois): widget.setROI(name, roi) width = max(region.x) - min(region.x) height = max(region.y) - min(region.y) if width > height: dwidth = 500 dheight = 500 * height // width else: dheight = 500 dwidth = 500 * width // height widget.resize(dwidth, dheight) widget.closed.connect(self.on_roiWindowClosed) widget.setWindowForRoi(region) widget.update() widget.show() widget.activateWindow() self._livewidgets[key] = widget def closeRoiWindow(self, roi): widget = self._getLiveWidget(roi) if widget: widget.close() def on_closed(self): for w in self._livewidgets.values(): w.close() def _register_rois(self, detectors): self.rois.clear() self.actionROI.setVisible(False) self.menuROI = QMenu(self) self.actionsROI = QActionGroup(self) self.actionsROI.setExclusive(False) for detname in detectors: self.log.debug('checking rois for detector \'%s\'', detname) for tup in self.client.eval(detname + '.postprocess', ''): roi = tup[0] cachekey = roi + '/roi' # check whether or not this is a roi (cachekey exists). keyval = self.client.getCacheKey(cachekey) if keyval: self.on_roiChange(cachekey, keyval[1]) self.log.debug('register roi: %s', roi) # create roi menu action = self.menuROI.addAction(roi) action.setData(roi) action.setCheckable(True) self.actionsROI.addAction(action) action.triggered.connect(self.on_roi_triggered) self.actionROI.setMenu(self.menuROI) self.actionROI.setVisible(True) def on_actionROI_triggered(self): w = self.toolbar.widgetForAction(self.actionROI) self.actionROI.menu().popup(w.mapToGlobal(QPoint(0, w.height()))) def on_roi_triggered(self): action = self.sender() roi = action.data() if action.isChecked(): self.showRoiWindow(roi) else: self.closeRoiWindow(roi) def on_roiWindowClosed(self): widget = self.sender() if widget: key = None for key, w in iteritems(self._livewidgets): if w == widget: self.log.debug('delete roi: %s', key) del self._livewidgets[key] break if key: roi = key.rsplit('/', 1)[0] for action in self.actionsROI.actions(): if action.data() == roi: action.setChecked(False) self.log.debug('uncheck roi: %s', roi) def on_roiChange(self, key, value): self.log.debug('on_roiChange: %s %s', key, (value, )) self.rois[key] = value for widget in self._get_all_widgets(): widget.setROI(key, value) widget = self._livewidgets.get(key, None) if widget: widget.setWindowForRoi(self.widget._rois[key]) def on_cache(self, data): _time, key, _op, svalue = data try: value = cache_load(svalue) except ValueError: value = None if key in self.rois: self.on_roiChange(key, value) elif key == self.detectorskey and self.widget: self._register_rois(value) def on_client_connected(self): self.client.tell('eventunmask', ['livedata', 'liveparams']) datapath = self.client.eval('session.experiment.datapath', '') if not datapath or not path.isdir(datapath): return if self._instrument == 'imaging': for fn in sorted(os.listdir(datapath)): if fn.endswith('.fits'): self.add_to_flist(path.join(datapath, fn), '', 'fits', False) self.detectorskey = (self.client.eval('session.experiment.name') + '/detlist').lower() def on_client_liveparams(self, params): tag, uid, det, fname, dtype, nx, ny, nz, runtime = params # TODO: remove compatibility code if isinstance(fname, string_types): fname, nx, ny, nz = [fname], [nx], [ny], [nz] if self._allowed_detectors and det not in self._allowed_detectors: self._ignore_livedata = True return self._ignore_livedata = False self._runtime = runtime self._last_uid = uid if dtype: self.setLiveItems(len(fname)) self._last_fnames = None normalized_type = numpy.dtype(dtype).str if normalized_type not in DATATYPES: self._last_format = None self.log.warning('Unsupported live data format: %s', (params, )) return self._last_format = normalized_type elif fname: self._last_fnames = fname self._last_format = None self._last_tag = tag.lower() self._nx = nx self._ny = ny self._nz = nz self._last_idx = 0 def _initLiveWidget(self, array): """Initialize livewidget based on array's shape""" if len(array.shape) == 1: widgetcls = LiveWidget1D else: widgetcls = IntegralLiveWidget self.initLiveWidget(widgetcls) def setData(self, array, uid=None, display=True): """Dispatch data array to corresponding live widgets. Cache array based on uid parameter. No caching if uid is ``None``. """ if uid: if uid not in self._datacache: self.log.debug('add to cache: %s', uid) self._datacache[uid] = array if display: self._initLiveWidget(array) for widget in self._get_all_widgets(): widget.setData(array) def setDataFromFile(self, filename, tag, uid=None, display=True): """Load data array from file and dispatch to live widgets using ``setData``. Do not use caching if uid is ``None``. """ try: array = ReaderRegistry.getReaderCls(tag).fromfile(filename) except KeyError: raise NicosError('Unsupported fileformat %r' % tag) if array is not None: self.setData(array, uid, display=display) else: raise NicosError('Cannot read file %r' % filename) def on_client_livedata(self, data): if self._ignore_livedata: # ignore all live events return idx = self._last_idx # 0 <= array number < n self._last_idx += 1 # check for allowed tags but always allow live data if self._last_tag in self._allowed_tags or self._last_tag == 'live': # pylint: disable=len-as-condition if len(data) and self._last_format: # we got live data with a specified format uid = str(self._last_uid) + '-' + str(idx) array = numpy.frombuffer(data, self._last_format) if self._nz[idx] > 1: array = array.reshape( (self._nz[idx], self._ny[idx], self._nx[idx])) elif self._ny[idx] > 1: array = array.reshape((self._ny[idx], self._nx[idx])) # update display for selected live channel, just cache # otherwise self.setData(array, uid, display=(idx == self._livechannel)) self.liveitems[idx].setData(FILEUID, uid) else: # we got no live data, but a filename with the data # filename corresponds to full qualififed path here for i, filename in enumerate(self._last_fnames): uid = str(self._last_uid) + '-' + str(i) self.add_to_flist(filename, self._last_format, self._last_tag, uid) try: # update display for selected live channel, just cache # otherwise self.setDataFromFile(filename, self._last_tag, uid, display=(i == self._livechannel)) except Exception as e: if uid in self._datacache: # image is already cached # suppress error message for cached image self.log.debug(e) else: # image is not cached and could not be loaded self.log.exception(e) def remove_obsolete_cached_files(self): """Removes outdated cached files from the file list or set cached flag to False if the file is still available on the filesystem. """ cached_item_rows = [] for row in range(self.fileList.count()): item = self.fileList.item(row) if item.data(FILEUID): cached_item_rows.append(row) if len(cached_item_rows) > self._cachesize: for row in cached_item_rows[0:-self._cachesize]: item = self.fileList.item(row) self.log.debug('remove from cache %s %s', item.data(FILEUID), item.data(FILENAME)) if path.isfile(item.data(FILENAME)): item.setData(FILEUID, None) else: self.fileList.takeItem(row) def add_to_flist(self, filename, fformat, ftag, uid=None, scroll=True): shortname = path.basename(filename) item = QListWidgetItem(shortname) item.setData(FILENAME, filename) item.setData(FILEFORMAT, fformat) item.setData(FILETAG, ftag) item.setData(FILEUID, uid) self.fileList.insertItem(self.fileList.count() - len(self.liveitems), item) if uid: self.remove_obsolete_cached_files() if scroll: self.fileList.scrollToBottom() return item def on_fileList_itemClicked(self, item): if item is None: return fname = item.data(FILENAME) ftag = item.data(FILETAG) if item in self.liveitems and ftag == 'live': # show live image self._livechannel = int(fname) fname = None self.log.debug("set livechannel: %d", self._livechannel) else: self._livechannel = None self.log.debug("no direct display") uid = item.data(FILEUID) if uid: # show image from cache array = self._datacache.get(uid, None) if array is not None and array.size: self.setData(array) return if fname: # show image from file self.setDataFromFile(fname, ftag) def on_fileList_currentItemChanged(self, item, previous): self.on_fileList_itemClicked(item) @pyqtSlot() def on_actionOpen_triggered(self): """Open image file using registered reader classes.""" ftypes = { ffilter: ftype for ftype, ffilter in ReaderRegistry.filefilters() } fdialog = FileFilterDialog(self, "Open data files", "", ";;".join(ftypes.keys())) if self._fileopen_filter: fdialog.selectNameFilter(self._fileopen_filter) if fdialog.exec_() == fdialog.Accepted: self._fileopen_filter = fdialog.selectedNameFilter() tag = ftypes[self._fileopen_filter] files = fdialog.selectedFiles() if files: def _cacheFile(fn, tag): uid = uuid4() # setDataFromFile may raise an `NicosException`, e.g. # if the file cannot be opened. self.setDataFromFile(fn, tag, uid, display=False) return self.add_to_flist(fn, None, tag, uid) # load and display first item f = files.pop(0) self.fileList.setCurrentItem(_cacheFile(f, tag)) cachesize = self._cachesize - 1 # add first `cachesize` files to cache for _, f in enumerateWithProgress(files[:cachesize], "Loading data files...", parent=fdialog): _cacheFile(f, tag) # add further files to file list (open on request/itemClicked) for f in files[cachesize:]: self.add_to_flist(f, None, tag) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.unzoom() @pyqtSlot() def on_actionPrint_triggered(self): self.widget.printDialog() @pyqtSlot() def on_actionLogScale_triggered(self): for widget in self._get_all_widgets(): widget.logscale(self.actionLogScale.isChecked()) @pyqtSlot() def on_actionMarkCenter_triggered(self): flag = self.actionMarkCenter.isChecked() for widget in self._get_all_widgets(): widget.setCenterMark(flag) @pyqtSlot() def on_actionKeepRatio_triggered(self): self.widget.gr.setAdjustSelection(self.actionKeepRatio.isChecked())
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/live.ui') self._allowed_tags = set() self._last_tag = None self._last_fname = None self._last_format = None self._runtime = 0 self._no_direct_display = False self._range_active = False self._cachesize = 20 self._datacache = BoundedOrderedDict(maxlen=self._cachesize) self._datapathok = False self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.widget = LWWidget(self) self.widget.setContextMenuPolicy(Qt.CustomContextMenu) self.widget.setControls(Logscale | MinimumMaximum | BrightnessContrast | Integrate | Histogram) self.widgetLayout.addWidget(self.widget) self.liveitem = QListWidgetItem('<Live>', self.fileList) self.liveitem.setData(32, '') self.liveitem.setData(33, '') self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) # configure instrument specific behavior self._instrument = options.get('instrument', '') self.widget.setInstrumentOption(self._instrument) if self._instrument == 'toftof': self.widget.setAxisLabels('time channels', 'detectors') elif self._instrument == 'imaging': self.widget.setControls(ShowGrid | Logscale | Grayscale | Normalize | Darkfield | Despeckle | CreateProfile | Histogram | MinimumMaximum) elif self._instrument == 'laue': self.widget.setControls(ShowGrid | Grayscale | Darkfield | Despeckle | CreateProfile | Histogram | MinimumMaximum) self.widget.setStandardColorMap(True, False) elif self._instrument == 'poli': self.widget.setControls(ShowGrid | Logscale | Grayscale | Despeckle | CreateProfile | Histogram | MinimumMaximum | BrightnessContrast) if self._instrument in ('dns', 'dnspsd'): self.widget.setKeepAspect(False) else: self.widget.setKeepAspect(True) # configure allowed file types opt_filetypes = options.get('filetypes', list(FILETYPES)) self._allowed_tags = set(opt_filetypes) & set(FILETYPES) # configure caching self._showcached = options.get('showcached', False) self._cachesize = options.get('cachesize', 20) if self._cachesize < 1: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) client.connected.connect(self.on_client_connected) client.setup.connect(self.on_client_connected) self.actionLogScale.toggled.connect(self.widget.setLog10) self.widget.profileUpdate.connect(self.on_widget_profileUpdate) self.widget.customContextMenuRequested.connect( self.on_widget_customContextMenuRequested) self._toftof_profile = None
class LiveDataPanel(Panel): """Provides a generic "detector live view" for 2-D images. For most instruments, a specific panel must be implemented that takes care of the individual live display needs. Options: * ``instrument`` -- the instrument name that is passed on to the livewidget module. * ``filetypes`` default[] - List of filename extensions whose content should be displayed. This list extends the list of 'fits', 'raw', 'tiff', and 'TIFF'. * ``cachesize`` (default 20) - Number of entries in the live data cache. The live data cache allows to display of previous taken data. * ``showcached`` (default False) - If True the taken live data will be cached. """ panelName = 'Live data view' bar = None menu = None def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, 'panels/live.ui') self._allowed_tags = set() self._last_tag = None self._last_fname = None self._last_format = None self._runtime = 0 self._no_direct_display = False self._range_active = False self._cachesize = 20 self._datacache = BoundedOrderedDict(maxlen=self._cachesize) self._datapathok = False self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.widget = LWWidget(self) self.widget.setContextMenuPolicy(Qt.CustomContextMenu) self.widget.setControls(Logscale | MinimumMaximum | BrightnessContrast | Integrate | Histogram) self.widgetLayout.addWidget(self.widget) self.liveitem = QListWidgetItem('<Live>', self.fileList) self.liveitem.setData(32, '') self.liveitem.setData(33, '') self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) # configure instrument specific behavior self._instrument = options.get('instrument', '') self.widget.setInstrumentOption(self._instrument) if self._instrument == 'toftof': self.widget.setAxisLabels('time channels', 'detectors') elif self._instrument == 'imaging': self.widget.setControls(ShowGrid | Logscale | Grayscale | Normalize | Darkfield | Despeckle | CreateProfile | Histogram | MinimumMaximum) elif self._instrument == 'laue': self.widget.setControls(ShowGrid | Grayscale | Darkfield | Despeckle | CreateProfile | Histogram | MinimumMaximum) self.widget.setStandardColorMap(True, False) elif self._instrument == 'poli': self.widget.setControls(ShowGrid | Logscale | Grayscale | Despeckle | CreateProfile | Histogram | MinimumMaximum | BrightnessContrast) if self._instrument in ('dns', 'dnspsd'): self.widget.setKeepAspect(False) else: self.widget.setKeepAspect(True) # configure allowed file types opt_filetypes = options.get('filetypes', list(FILETYPES)) self._allowed_tags = set(opt_filetypes) & set(FILETYPES) # configure caching self._showcached = options.get('showcached', False) self._cachesize = options.get('cachesize', 20) if self._cachesize < 1: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) client.connected.connect(self.on_client_connected) client.setup.connect(self.on_client_connected) self.actionLogScale.toggled.connect(self.widget.setLog10) self.widget.profileUpdate.connect(self.on_widget_profileUpdate) self.widget.customContextMenuRequested.connect( self.on_widget_customContextMenuRequested) self._toftof_profile = None def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('geometry', self.saveGeometry()) if self._toftof_profile: self._toftof_profile.close() def getMenus(self): if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) self.menu = menu return [self.menu] def getToolbars(self): if not self.bar: bar = QToolBar('Live data') bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionLogScale) bar.addSeparator() bar.addAction(self.actionUnzoom) self.bar = bar return [self.bar] def on_widget_customContextMenuRequested(self, point): self.menu.popup(self.mapToGlobal(point)) def on_widget_profileUpdate(self, proftype, nbins, x, y): if self._instrument != 'toftof': return if self._toftof_profile is None: self._toftof_profile = ToftofProfileWindow(self) self._toftof_profile.update(proftype, nbins, x, y) self._toftof_profile.show() def on_client_connected(self): self.client.tell('eventunmask', ['livedata', 'liveparams']) datapath = self.client.eval('session.experiment.datapath', '') if not datapath or not path.isdir(datapath): self._showcached = True # always show cached data if datapath is not accessible return if self._instrument == 'imaging': for fn in sorted(os.listdir(datapath)): if fn.endswith('.fits'): self.add_to_flist(path.join(datapath, fn), '', 'fits', False) def on_client_liveparams(self, params): tag, _uid, det, fname, dtype, nx, ny, nz, runtime = params if (self._instrument == 'dns' and det != 'det') or \ (self._instrument == 'dnspsd' and det != 'qm_det'): self._last_tag = self._last_fname = '' return if not isinstance(fname, string_types): fname, nx, ny, nz = fname[0], nx[0], ny[0], nz[0] self._runtime = runtime normalized_type = numpy.dtype(dtype).str if dtype != '' else '' # pylint: disable=compare-to-empty-string if not fname and normalized_type not in DATATYPES: self._last_format = self._last_fname = None self.log.warning('Unsupported live data format: %s', params) return self._last_tag = tag.lower() self._last_fname = fname self._last_format = normalized_type self._nx = nx self._ny = ny self._nz = nz def on_client_livedata(self, data): # pylint: disable=len-as-condition d = None if self._last_fname: if path.isfile( self._last_fname) and self._last_tag in self._allowed_tags: # in the case of a filename, we add it to the list self.add_to_flist(self._last_fname, self._last_format, self._last_tag) d = LWData(self._last_fname) elif len( data ) and self._last_format and self._last_tag in self._allowed_tags or self._last_tag == 'live': d = LWData(self._nx, self._ny, self._nz, self._last_format, data) self._datacache[self._last_fname] = (self._nx, self._ny, self._nz, self._last_format, data) if self._showcached: self.add_to_flist(self._last_fname, self._last_format, self._last_tag, cached=True) # always allow live data if self._last_tag == 'live': if len(data) and self._last_format: # we got live data with a specified format d = LWData(self._nx, self._ny, self._nz, self._last_format, data) # but display it right now only if on <Live> setting if self._no_direct_display or not d: return self.widget.setData(d) def add_to_flist(self, filename, fformat, ftag, cached=False, scroll=True): shortname = path.basename(filename) item = QListWidgetItem(shortname) item.setData(32, filename) item.setData(33, fformat) item.setData(34, ftag) item.setData(35, cached) self.fileList.insertItem(self.fileList.count() - 1, item) if cached: self.del_obsolete_cached_data() if scroll: self.fileList.scrollToBottom() def del_obsolete_cached_data(self): cached_item_rows = list() for row in range(self.fileList.count()): item = self.fileList.item(row) if item.data(35): cached_item_rows.append(row) if len(cached_item_rows) > self._cachesize: for row in cached_item_rows[0:-self._cachesize]: self.fileList.takeItem(row) def on_fileList_itemClicked(self, item): if item is None: return fname = item.data(32) ftag = item.data(34) cached = item.data(35) if fname == '': # pylint: disable=compare-to-empty-string # show always latest live image if self._no_direct_display: self._no_direct_display = False d = None if self._last_fname and path.isfile(self._last_fname) and \ self._last_tag in self._allowed_tags: d = LWData(self._last_fname) elif self._datacache: val = self._datacache.getlast() d = LWData(*val) if d: self.widget.setData(d) else: # show image from file self._no_direct_display = True if fname and str(ftag) in self._allowed_tags or str( ftag) == 'live': if cached: d = self._datacache.get(fname, None) if d: self.widget.setData(LWData(*d)) else: self.widget.setData(LWData(fname)) def on_fileList_currentItemChanged(self, item, previous): self.on_fileList_itemClicked(item) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.plot().getZoomer().zoom(0) @pyqtSlot() def on_actionPrint_triggered(self): printer = QPrinter(QPrinter.HighResolution) printer.setColorMode(QPrinter.Color) printer.setOrientation(QPrinter.Landscape) printer.setOutputFileName('') if QPrintDialog(printer, self).exec_() == QDialog.Accepted: self.widget.plot().print_(printer) self.statusBar.showMessage('Plot successfully printed to %s.' % str(printer.printerName()))
class LiveDataPanel(Panel): panelName = 'Live data view' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_mlz/antares/gui/live.ui')) self._format = None self._runtime = 0 self._no_direct_display = False self._range_active = False self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.widget = LWWidget(self) self.widget.setContextMenuPolicy(Qt.CustomContextMenu) self.widgetLayout.addWidget(self.widget) self.rangeFrom = QDoubleSpinBox(self) self.rangeTo = QDoubleSpinBox(self) for ctrl in [self.rangeFrom, self.rangeTo]: ctrl.setEnabled(False) ctrl.setMaximumWidth(90) ctrl.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) ctrl.valueChanged.connect(self.on_rangeChanged) self.liveitem = QListWidgetItem('<Live>', self.fileList) self.liveitem.setData(32, '') self.liveitem.setData(33, '') self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) client.livedata.connect(self.on_client_livedata) client.liveparams.connect(self.on_client_liveparams) if client.isconnected: self.on_client_connected() client.connected.connect(self.on_client_connected) client.setup.connect(self.on_client_connected) self.actionLogScale.toggled.connect(self.widget.setLog10) self.widget.customContextMenuRequested.connect( self.on_widget_customContextMenuRequested) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) def getMenus(self): self.menu = menu = QMenu('&Live data', self) # menu.addAction(self.actionLoadTOF) # menu.addAction(self.actionLoadPAD) # menu.addSeparator() # menu.addAction(self.actionWriteXml) menu.addAction(self.actionPrint) menu.addSeparator() menu.addAction(self.actionSetAsROI) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionNormalized) menu.addAction(self.actionLegend) return [menu] def getToolbars(self): bar = QToolBar('Live data') # bar.addAction(self.actionWriteXml) bar.addAction(self.actionPrint) bar.addSeparator() bar.addAction(self.actionLogScale) bar.addSeparator() bar.addAction(self.actionUnzoom) bar.addAction(self.actionSetAsROI) # bar.addSeparator() # bar.addAction(self.actionSelectChannels) return [bar] def on_widget_customContextMenuRequested(self, point): self.menu.popup(self.mapToGlobal(point)) def on_client_connected(self): self.client.tell('eventunmask', ['livedata', 'liveparams']) datapath = self.client.eval('session.experiment.datapath', '') if not datapath: return caspath = path.join(datapath, 'cascade') if path.isdir(caspath): for fn in sorted(os.listdir(caspath)): if fn.endswith('.pad'): self.add_to_flist(path.join(caspath, fn), 'pad', False) elif fn.endswith('tof'): self.add_to_flist(path.join(caspath, fn), 'tof', False) def on_client_liveparams(self, params): _tag, _uid, _det, _fname, dtype, nx, ny, nz, runtime = params # TODO: remove compatibility code if not isinstance(nx, integer_types): nx, ny, nz = nx[0], ny[0], nz[0] self._runtime = runtime if dtype not in DATATYPES: self._format = None self.log.warning('Unsupported live data format: %r', params) return self._format = dtype self._nx = nx self._ny = ny self._nz = nz def on_client_livedata(self, data): if self._format: self.widget.setData( LWData(self._nx, self._ny, self._nz, self._format, data)) @pyqtSlot() def on_actionSetAsROI_triggered(self): zoom = self.widget.plot().getZoomer().zoomRect() # XXX this is detector specific! self.client.run('det.setRelativeRoi(%s, %s, %s, %s)' % (int(zoom.left()), int(zoom.top()), int( zoom.right()), int(zoom.bottom()))) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.plot().getZoomer().zoom(0) @pyqtSlot() def on_actionPrint_triggered(self): printer = QPrinter(QPrinter.HighResolution) printer.setColorMode(QPrinter.Color) printer.setOrientation(QPrinter.Landscape) printer.setOutputFileName('') if QPrintDialog(printer, self).exec_() == QDialog.Accepted: self.widget.plot().print_(printer) def on_actionCustomRange_toggled(self, on): self.rangeFrom.setEnabled(on) self.rangeTo.setEnabled(on) self.widget.SetAutoCountRange(not on) self._range_active = on if on: self.on_rangeChanged(0) else: self.updateRange() def on_rangeChanged(self, val): if self._range_active: self.widget.SetCountRange(self.rangeFrom.value(), self.rangeTo.value()) def updateRange(self): if not self.actionCustomRange.isChecked(): crange = self.widget.GetData2d().range() self.rangeFrom.setValue(crange.minValue()) self.rangeTo.setValue(crange.maxValue()) def closeEvent(self, event): with self.sgroup as settings: settings.setValue('geometry', self.saveGeometry()) event.accept()
class LiveDataPanel(Panel): """Provides a generic "detector live view". For most instruments, a specific panel must be implemented that takes care of the individual live display needs. Options: * ``filetypes`` (default []) - List of filename extensions whose content should be displayed. * ``detectors`` (default []) - List of detector devices whose data should be displayed. If not set data from all configured detectors will be shown. * ``cachesize`` (default 20) - Number of entries in the live data cache. The live data cache allows displaying of previously measured data. * ``liveonlyindex`` (default None) - Enable live only view. This disables interaction with the liveDataPanel and only displays the dataset of the set index. * ``defaults`` (default []) - List of strings representing options to be set for every configured plot. These options can not be set on a per plot basis since they are global. Options are as follows: * ``logscale`` - Switch the logarithic scale on. * ``center`` - Display the center lines for the image. * ``nolines`` - Display lines for the curve. * ``markers`` - Display symbols for data points. * ``unzoom`` - Unzoom the plot when new data is received. * ``plotsettings`` (default []) - List of dictionaries which contain settings for the individual datasets. Each entry will be applied to one of the detector's datasets. * ``plotcount`` (default 1) - Amount of plots in the dataset. * ``marks`` (default 'omark') - Shape of the markers (if displayed). Possible values are: 'dot', 'plus', 'asterrisk', 'circle', 'diagonalcross', 'solidcircle', 'triangleup', 'solidtriangleup', 'triangledown', 'solidtriangledown', 'square', 'solidsquare', 'bowtie', 'solidbowtie', 'hourglass', 'solidhourglass', 'diamond', 'soliddiamond', 'star', 'solidstar', 'triupdown', 'solidtriright', 'solidtrileft', 'hollowplus', 'solidplus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'star4', 'star5', 'star6', 'star7', 'star8', 'vline', 'hline', 'omark' * ``markersize`` (default 1) - Size of the markers (if displayed). * ``offset`` (default 0) - Offset for the X axis labels of each curve in 1D plots. * ``colors`` (default ['blue']) - Color of the marks and lines (if displayed). If colors are set as a list the colors will be applied to the individual plots (and default back to blue when wrong/missing), for example: ['red', 'green']: The first plot will be red, the second green and the others will be blue (default). 'red': all plots will be red. """ panelName = 'Live data view' ui = f'{uipath}/panels/live.ui' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, self.ui) self._allowed_filetypes = set() self._allowed_detectors = set() self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.unzoom = False self.lastSettingsIndex = None self._axis_labels = {} self.params = {} self._offset = 0 self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addAction(self.actionSavePlot) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # hide fileselection in liveonly mode self._liveOnlyIndex = options.get('liveonlyindex', None) if self._liveOnlyIndex is not None: self.pastFilesWidget.hide() self.statusBar.hide() # disable interactions with the plot self.setAttribute(Qt.WA_TransparentForMouseEvents) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_filetypes = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) defaults = options.get('defaults', []) if 'logscale' in defaults: self.actionLogScale.setChecked(True) if 'center' in defaults: self.actionMarkCenter.setChecked(True) if 'nolines' not in defaults: self.actionLines.setChecked(True) if 'markers' in defaults: self.actionSymbols.setChecked(True) if 'unzoom' in defaults: self.unzoom = True self.plotsettings = options.get('plotsettings', [DEFAULTS]) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1 or self._liveOnlyIndex is not None: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) self._initControlsGUI() def _initControlsGUI(self): pass def setLiveItems(self, n): nitems = len(self.liveitems) if n < nitems: nfiles = self.fileList.count() for i in range(nitems - 1, n - 1, -1): self.liveitems.pop(i) self.fileList.takeItem(nfiles - nitems + i) if self._livechannel > n: self._livechannel = 0 if n > 0 else None else: for i in range(nitems, n): item = QListWidgetItem('<Live #%d>' % (i + 1)) item.setData(FILENAME, i) item.setData(FILETYPE, '') item.setData(FILETAG, LIVE) self.fileList.insertItem(self.fileList.count(), item) self.liveitems.append(item) if self._liveOnlyIndex is not None: self.fileList.setCurrentRow(self._liveOnlyIndex) if n == 1: self.liveitems[0].setText('<Live>') else: self.liveitems[0].setText('<Live #1>') def set2DControlsEnabled(self, flag): if flag != self.actionKeepRatio.isChecked(): self.actionKeepRatio.trigger() for action in self._actions2D: action.setVisible(flag) def setControlsEnabled(self, flag): for action in self.toolbar.actions(): action.setEnabled(flag) self.actionOpen.setEnabled(True) # File Open action always available def initLiveWidget(self, widgetcls): if isinstance(self.widget, widgetcls): return # delete the old widget if self.widget: self.widgetLayout.removeWidget(self.widget) self.widget.deleteLater() # create a new one self.widget = widgetcls(self) # enable/disable controls and set defaults for new livewidget instances self.setControlsEnabled(True) if isinstance(self.widget, LiveWidget1D): self.set2DControlsEnabled(False) else: self.set2DControlsEnabled(True) # apply current global settings self.widget.setCenterMark(self.actionMarkCenter.isChecked()) self.widget.logscale(self.actionLogScale.isChecked()) if isinstance(self.widget, LiveWidget1D): self.widget.setSymbols(self.actionSymbols.isChecked()) self.widget.setLines(self.actionLines.isChecked()) # liveonly mode does not display a status bar if self._liveOnlyIndex is None: self.widget.gr.cbm.addHandler(MouseEvent.MOUSE_MOVE, self.on_mousemove_gr) # handle menus self.menuColormap = QMenu(self) self.actionsColormap = QActionGroup(self) activeMap = self.widget.getColormap() activeCaption = None for name, value in COLORMAPS.items(): caption = name.title() action = self.menuColormap.addAction(caption) action.setData(caption) action.setCheckable(True) if activeMap == value: action.setChecked(True) # update toolButton text later otherwise this may fail # depending on the setup and qt versions in use activeCaption = caption self.actionsColormap.addAction(action) action.triggered.connect(self.on_colormap_triggered) self.actionColormap.setMenu(self.menuColormap) # finish initiation self.widgetLayout.addWidget(self.widget) if activeCaption: self.toolbar.widgetForAction( self.actionColormap).setText(activeCaption) detectors = self.client.eval('session.experiment.detectors', []) self._register_rois(detectors) def loadSettings(self, settings): self.splitterstate = settings.value('splitter', '', QByteArray) def saveSettings(self, settings): settings.setValue('splitter', self.splitter.saveState()) settings.setValue('geometry', self.saveGeometry()) def getMenus(self): if self._liveOnlyIndex is not None: return [] if not self.menu: menu = QMenu('&Live data', self) menu.addAction(self.actionOpen) menu.addAction(self.actionPrint) menu.addAction(self.actionSavePlot) menu.addSeparator() menu.addAction(self.actionKeepRatio) menu.addAction(self.actionUnzoom) menu.addAction(self.actionLogScale) menu.addAction(self.actionColormap) menu.addAction(self.actionMarkCenter) menu.addAction(self.actionROI) menu.addAction(self.actionSymbols) menu.addAction(self.actionLines) self.menu = menu return [self.menu] def _get_all_widgets(self): yield self.widget yield from self._livewidgets.values() def getToolbars(self): if self._liveOnlyIndex is not None: return [] return [self.toolbar] def on_mousemove_gr(self, event): xyz = None if event.getWindow(): # inside plot xyz = self.widget.getZValue(event) if xyz: fmt = '(%g, %g)' # x, y data 1D integral plots if len(xyz) == 3: fmt += ': %g' # x, y, z data for 2D image plot self.statusBar.showMessage(fmt % xyz) else: self.statusBar.clearMessage() def on_actionColormap_triggered(self): w = self.toolbar.widgetForAction(self.actionColormap) m = self.actionColormap.menu() if m: m.popup(w.mapToGlobal(QPoint(0, w.height()))) def on_colormap_triggered(self): action = self.actionsColormap.checkedAction() name = action.data() for widget in self._get_all_widgets(): widget.setColormap(COLORMAPS[name.upper()]) self.toolbar.widgetForAction(self.actionColormap).setText(name.title()) @pyqtSlot() def on_actionLines_triggered(self): if self.widget and isinstance(self.widget, LiveWidget1D): self.widget.setLines(self.actionLines.isChecked()) @pyqtSlot() def on_actionSymbols_triggered(self): if self.widget and isinstance(self.widget, LiveWidget1D): self.widget.setSymbols(self.actionSymbols.isChecked()) def _getLiveWidget(self, roi): return self._livewidgets.get(roi + '/roi', None) def showRoiWindow(self, roikey): key = roikey + '/roi' widget = self._getLiveWidget(roikey) region = self.widget._rois[key] if not widget: widget = LiveWidget(None) widget.setWindowTitle(roikey) widget.setColormap(self.widget.getColormap()) widget.setCenterMark(self.actionMarkCenter.isChecked()) widget.logscale(self.actionLogScale.isChecked()) widget.gr.setAdjustSelection(False) # don't use adjust on ROIs for name, roi in self.rois.items(): widget.setROI(name, roi) width = max(region.x) - min(region.x) height = max(region.y) - min(region.y) if width > height: dwidth = 500 dheight = 500 * height // width else: dheight = 500 dwidth = 500 * width // height widget.resize(dwidth, dheight) widget.closed.connect(self.on_roiWindowClosed) widget.setWindowForRoi(region) widget.update() widget.show() widget.activateWindow() self._livewidgets[key] = widget def closeRoiWindow(self, roi): widget = self._getLiveWidget(roi) if widget: widget.close() def on_closed(self): for w in self._livewidgets.values(): w.close() def _register_rois(self, detectors): self.rois.clear() self.actionROI.setVisible(False) self.menuROI = QMenu(self) self.actionsROI = QActionGroup(self) self.actionsROI.setExclusive(False) for detname in detectors: self.log.debug('checking rois for detector \'%s\'', detname) for tup in self.client.eval(detname + '.postprocess', ''): roi = tup[0] cachekey = roi + '/roi' # check whether or not this is a roi (cachekey exists). keyval = self.client.getCacheKey(cachekey) if keyval: self.on_roiChange(cachekey, keyval[1]) self.log.debug('register roi: %s', roi) # create roi menu action = self.menuROI.addAction(roi) action.setData(roi) action.setCheckable(True) self.actionsROI.addAction(action) action.triggered.connect(self.on_roi_triggered) self.actionROI.setMenu(self.menuROI) self.actionROI.setVisible(True) def on_actionROI_triggered(self): w = self.toolbar.widgetForAction(self.actionROI) self.actionROI.menu().popup(w.mapToGlobal(QPoint(0, w.height()))) def on_roi_triggered(self): action = self.sender() roi = action.data() if action.isChecked(): self.showRoiWindow(roi) else: self.closeRoiWindow(roi) def on_roiWindowClosed(self): widget = self.sender() if widget: key = None for key, w in self._livewidgets.items(): if w == widget: self.log.debug('delete roi: %s', key) del self._livewidgets[key] break if key: roi = key.rsplit('/', 1)[0] for action in self.actionsROI.actions(): if action.data() == roi: action.setChecked(False) self.log.debug('uncheck roi: %s', roi) def on_roiChange(self, key, value): self.log.debug('on_roiChange: %s %s', key, (value, )) self.rois[key] = value for widget in self._get_all_widgets(): widget.setROI(key, value) widget = self._livewidgets.get(key, None) if widget: widget.setWindowForRoi(self.widget._rois[key]) def on_cache(self, data): _time, key, _op, svalue = data try: value = cache_load(svalue) except ValueError: value = None if key in self.rois: self.on_roiChange(key, value) elif key == self.detectorskey and self.widget: self._register_rois(value) def on_client_connected(self): self.client.tell('eventunmask', ['livedata']) self.detectorskey = (self.client.eval('session.experiment.name') + '/detlist').lower() def normalizeType(self, dtype): normalized_type = numpy.dtype(dtype).str if normalized_type not in DATATYPES: self.log.warning('Unsupported live data format: %s', normalized_type) return return normalized_type def getIndexedUID(self, idx): return str(self.params['uid']) + '-' + str(idx) def _process_axis_labels(self, blobs): """Convert the raw axis label descriptions. tuple: `from, to`: Distribute labels equidistantly between the two values. numbertype: `index into labels`: Actual labels are provided. Value is the starting index. Extract from first available blob. Remove said blob from list. None: `default`: Start at 0 with stepwidth 1. Save the axis labels to the datacache. """ CLASSIC = {'define': 'classic'} for i, datadesc in enumerate(self.params['datadescs']): labels = {} titles = {} for size, axis in zip(reversed(datadesc['shape']), AXES): # if the 'labels' key does not exist or does not have the right # axis key set default to 'classic'. label = datadesc.get('labels', { 'x': CLASSIC, 'y': CLASSIC }).get(axis, CLASSIC) if label['define'] == 'range': start = label.get('start', 0) size = label.get('length', 1) step = label.get('step', 1) end = start + step * size labels[axis] = numpy.arange(start, end, step) elif label['define'] == 'array': index = label.get('index', 0) labels[axis] = numpy.frombuffer(blobs[index], label.get('dtype', '<i4')) else: labels[axis] = self.getDefaultLabels(size) labels[axis] += self._offset if axis == 'x' else 0 titles[axis] = label.get('title') # save the labels in the datacache with uid as key uid = self.getIndexedUID(i) if uid not in self._datacache: self._datacache[uid] = {} self._datacache[uid]['labels'] = labels self._datacache[uid]['titles'] = titles def _process_livedata(self, data, idx): # ignore irrelevant data in liveOnly mode if self._liveOnlyIndex is not None and idx != self._liveOnlyIndex: return try: descriptions = self.params['datadescs'] except KeyError: self.log.warning('Livedata with tag "Live" without ' '"datadescs" provided.') return # pylint: disable=len-as-condition if len(data): # we got live data with specified formats arrays = self.processDataArrays( idx, numpy.frombuffer(data, descriptions[idx]['dtype'])) if arrays is None: return # put everything into the cache uid = self.getIndexedUID(idx) self._datacache[uid]['dataarrays'] = arrays self.liveitems[idx].setData(FILEUID, uid) def _process_filenames(self): # TODO: allow multiple fileformats? # would need to modify input from DemonSession.notifyDataFile number_of_items = self.fileList.count() for i, filedesc in enumerate(self.params['filedescs']): uid = self.getIndexedUID(number_of_items + i) name = filedesc['filename'] filetype = filedesc.get('fileformat') if filetype is None or filetype not in ReaderRegistry.filetypes(): continue # Ignore unregistered file types self.add_to_flist(name, filetype, FILE, uid) try: # update display for selected live channel, # just cache otherwise self.setDataFromFile(name, filetype, uid, display=(i == self._livechannel)) except Exception as e: if uid in self._datacache: # image is already cached # suppress error message for cached image self.log.debug(e) else: # image is not cached and could not be loaded self.log.exception(e) def on_client_livedata(self, params, blobs): # blobs is a list of data blobs and labels blobs if self._allowed_detectors \ and params['det'] not in self._allowed_detectors: return self.params = params self._runtime = params['time'] if params['tag'] == LIVE: datacount = len(params['datadescs']) self.setLiveItems(datacount) self._process_axis_labels(blobs[datacount:]) for i, blob in enumerate(blobs[:datacount]): self._process_livedata(blob, i) if not datacount: self._process_livedata([], 0) elif params['tag'] == FILE: self._process_filenames() self._show() def getDefaultLabels(self, size): return numpy.array(range(size)) def convertLabels(self, labelinput): """Convert the input into a processable format""" for i, entry in enumerate(labelinput): if isinstance(entry, str): labelinput[i] = self.normalizeType(entry) return labelinput def _initLiveWidget(self, array): """Initialize livewidget based on array's shape""" if len(array.shape) == 1: widgetcls = LiveWidget1D else: widgetcls = IntegralLiveWidget self.initLiveWidget(widgetcls) def setDataFromFile(self, filename, filetype, uid=None, display=True): """Load data array from file and dispatch to live widgets using ``setData``. Do not use caching if uid is ``None``. """ array = readDataFromFile(filename, filetype) if array is not None: if uid: if uid not in self._datacache: self.log.debug('add to cache: %s', uid) self._datacache[uid] = {} self._datacache[uid]['dataarrays'] = [array] if display: self._initLiveWidget(array) for widget in self._get_all_widgets(): widget.setData(array) # self.setData([array], uid, display=display) return array.shape else: raise NicosError('Cannot read file %r' % filename) def processDataArrays(self, index, entry): """Check if the input 1D array has the expected amount of values. If the array is too small an Error is raised. If the size exceeds the expected amount it is truncated. Returns a list of arrays corresponding to the ``plotcount`` of ``index`` into ``datadescs`` of the current params""" datadesc = self.params['datadescs'][index] count = datadesc.get('plotcount', DEFAULTS['plotcount']) shape = datadesc['shape'] # ignore irrelevant data in liveOnly mode if self._liveOnlyIndex is not None and index != self._liveOnlyIndex: return # determine 1D array size arraysize = numpy.product(shape) # check and split the input array if len(entry) < count * arraysize: self.log.warning('Expected dataarray with %d entries, got %d', count * arraysize, len(entry)) return arrays = numpy.split(entry[:count * arraysize], count) # reshape every array in the list for i, array in enumerate(arrays): arrays[i] = array.reshape(shape) return arrays def applyPlotSettings(self): if not self.widget or not isinstance(self.widget, LiveWidget1D): return if self._liveOnlyIndex is not None: index = self._liveOnlyIndex elif self.fileList.currentItem() not in self.liveitems: return else: index = self.fileList.currentRow() if isinstance(self.widget, LiveWidget1D): def getElement(l, index, default): try: return l[index] except IndexError: return default settings = getElement(self.plotsettings, index, DEFAULTS) if self.params['tag'] == LIVE: plotcount = self.params['datadescs'][index].get( 'plotcount', DEFAULTS['plotcount']) else: plotcount = DEFAULTS['plotcount'] marks = [settings.get('marks', DEFAULTS['marks'])] markersize = settings.get('markersize', DEFAULTS['markersize']) offset = settings.get('offset', DEFAULTS['offset']) colors = settings.get('colors', DEFAULTS['colors']) if isinstance(colors, list): if len(colors) > plotcount: colors = colors[:plotcount] while len(colors) < plotcount: colors.append(DEFAULTS['colors']) else: colors = [colors] * plotcount self.setOffset(offset) self.widget.setMarks(marks) self.widget.setMarkerSize(markersize) self.widget.setPlotCount(plotcount, colors) def setOffset(self, offset): self._offset = offset def getDataFromItem(self, item): """Extract and return the data associated with the item. If the data is in the cache return it. If the data is in a valid file extract it from there. """ if item is None: return uid = item.data(FILEUID) # data is cached if uid and hasattr(self, '_datacache') and uid in self._datacache: return self._datacache[uid] # cache has cleared data or data has not been cached in the first place elif uid is None and item.data(FILETAG) == FILE: filename = item.data(FILENAME) filetype = item.data(FILETYPE) if path.isfile(filename): rawdata = readDataFromFile(filename, filetype) labels = {} titles = {} for axis, entry in zip(AXES, reversed(rawdata.shape)): labels[axis] = numpy.arange(entry) titles[axis] = axis data = { 'labels': labels, 'titles': titles, 'dataarrays': [rawdata] } return data # else: # TODO: mark for deletion on item changed? def _show(self, data=None): """Show the provided data. If no data has been provided extract it from the datacache via the current item's uid. :param data: dictionary containing 'dataarrays' and 'labels' """ idx = self.fileList.currentRow() if idx == -1: self.fileList.setCurrentRow(0) return # no data has been provided, try to get it from the cache if data is None: data = self.getDataFromItem(self.fileList.currentItem()) # still no data if data is None: return arrays = data.get('dataarrays', []) labels = data.get('labels', {}) titles = data.get('titles', {}) # if multiple datasets have to be displayed in one widget, they have # the same dimensions, so we only need the dimensions of one set self._initLiveWidget(arrays[0]) self.applyPlotSettings() for widget in self._get_all_widgets(): widget.setData(arrays, labels) widget.setTitles(titles) if self.unzoom and self.widget: self.on_actionUnzoom_triggered() def remove_obsolete_cached_files(self): """Remove or flag items which are no longer cached. The cache will delete items if it's size exceeds ´cachesize´. This checks the items in the filelist and their caching status, removing items with deleted associated files and flagging items with valid files to be reloaded if selected by the user. """ for index in reversed(range(self.fileList.count())): item = self.fileList.item(index) uid = item.data(FILEUID) # is the uid still cached if uid and uid not in self._datacache: # does the file still exist on the filesystem if path.isfile(item.data(FILENAME)): item.setData(FILEUID, None) else: self.fileList.takeItem(index) def add_to_flist(self, filename, filetype, tag, uid=None, scroll=True): # liveonly mode doesn't display a filelist if self._liveOnlyIndex is not None: return shortname = path.basename(filename) item = QListWidgetItem(shortname) item.setData(FILENAME, filename) item.setData(FILETYPE, filetype) item.setData(FILETAG, tag) item.setData(FILEUID, uid) self.fileList.insertItem(self.fileList.count(), item) if uid: self.remove_obsolete_cached_files() if scroll: self.fileList.scrollToBottom() return item def on_fileList_currentItemChanged(self): self._show() @pyqtSlot() def on_actionOpen_triggered(self): """Open image file using registered reader classes.""" ftypes = { ffilter: ftype for ftype, ffilter in ReaderRegistry.filefilters() if not self._allowed_filetypes or ftype in self._allowed_filetypes } fdialog = FileFilterDialog(self, "Open data files", "", ";;".join(ftypes.keys())) if self._fileopen_filter: fdialog.selectNameFilter(self._fileopen_filter) if fdialog.exec_() != fdialog.Accepted: return files = fdialog.selectedFiles() if not files: return self._fileopen_filter = fdialog.selectedNameFilter() filetype = ftypes[self._fileopen_filter] errors = [] def _cacheFile(fn, filetype): uid = uuid4() # setDataFromFile may raise an `NicosException`, e.g. # if the file cannot be opened. try: self.setDataFromFile(fn, filetype, uid, display=False) except Exception as err: errors.append('%s: %s' % (fn, err)) else: return self.add_to_flist(fn, filetype, FILE, uid) # load and display first item f = files.pop(0) item = _cacheFile(f, filetype) if item is not None: self.fileList.setCurrentItem(item) cachesize = self._cachesize - 1 # add first `cachesize` files to cache for _, f in enumerateWithProgress(files[:cachesize], "Loading data files...", parent=fdialog): _cacheFile(f, filetype) # add further files to file list (open on request/itemClicked) for f in files[cachesize:]: self.add_to_flist(f, filetype, FILE) if errors: self.showError('Some files could not be opened:\n\n' + '\n'.join(errors)) @pyqtSlot() def on_actionUnzoom_triggered(self): self.widget.unzoom() @pyqtSlot() def on_actionPrint_triggered(self): self.widget.printDialog() @pyqtSlot() def on_actionSavePlot_triggered(self): self.widget.savePlot() @pyqtSlot() def on_actionLogScale_triggered(self): for widget in self._get_all_widgets(): widget.logscale(self.actionLogScale.isChecked()) @pyqtSlot() def on_actionMarkCenter_triggered(self): flag = self.actionMarkCenter.isChecked() for widget in self._get_all_widgets(): widget.setCenterMark(flag) @pyqtSlot() def on_actionKeepRatio_triggered(self): self.widget.gr.setAdjustSelection(self.actionKeepRatio.isChecked())
def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, self.ui) self._allowed_filetypes = set() self._allowed_detectors = set() self._runtime = 0 self._range_active = False self._cachesize = 20 self._livewidgets = {} # livewidgets for rois: roi_key -> widget self._fileopen_filter = None self.widget = None self.menu = None self.unzoom = False self.lastSettingsIndex = None self._axis_labels = {} self.params = {} self._offset = 0 self.statusBar = QStatusBar(self, sizeGripEnabled=False) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) self.toolbar = QToolBar('Live data') self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionPrint) self.toolbar.addAction(self.actionSavePlot) self.toolbar.addSeparator() self.toolbar.addAction(self.actionLogScale) self.toolbar.addSeparator() self.toolbar.addAction(self.actionKeepRatio) self.toolbar.addAction(self.actionUnzoom) self.toolbar.addAction(self.actionColormap) self.toolbar.addAction(self.actionMarkCenter) self.toolbar.addAction(self.actionROI) self._actions2D = [self.actionROI, self.actionColormap] self.setControlsEnabled(False) self.set2DControlsEnabled(False) # hide fileselection in liveonly mode self._liveOnlyIndex = options.get('liveonlyindex', None) if self._liveOnlyIndex is not None: self.pastFilesWidget.hide() self.statusBar.hide() # disable interactions with the plot self.setAttribute(Qt.WA_TransparentForMouseEvents) self.liveitems = [] self.setLiveItems(1) self._livechannel = 0 self.splitter.setSizes([20, 80]) self.splitter.restoreState(self.splitterstate) if hasattr(self.window(), 'closed'): self.window().closed.connect(self.on_closed) client.livedata.connect(self.on_client_livedata) client.connected.connect(self.on_client_connected) client.cache.connect(self.on_cache) self.rois = {} self.detectorskey = None # configure allowed file types supported_filetypes = ReaderRegistry.filetypes() opt_filetypes = set(options.get('filetypes', supported_filetypes)) self._allowed_filetypes = opt_filetypes & set(supported_filetypes) # configure allowed detector device names detectors = options.get('detectors') if detectors: self._allowed_detectors = set(detectors) defaults = options.get('defaults', []) if 'logscale' in defaults: self.actionLogScale.setChecked(True) if 'center' in defaults: self.actionMarkCenter.setChecked(True) if 'nolines' not in defaults: self.actionLines.setChecked(True) if 'markers' in defaults: self.actionSymbols.setChecked(True) if 'unzoom' in defaults: self.unzoom = True self.plotsettings = options.get('plotsettings', [DEFAULTS]) # configure caching self._cachesize = options.get('cachesize', self._cachesize) if self._cachesize < 1 or self._liveOnlyIndex is not None: self._cachesize = 1 # always cache the last live image self._datacache = BoundedOrderedDict(maxlen=self._cachesize) self._initControlsGUI()
class SANSPanel(Panel): panelName = 'SANS acquisition demo' def __init__(self, parent, client, options): Panel.__init__(self, parent, client, options) loadUi(self, findResource('nicos_demo/demo/gui/sanspanel.ui')) self.current_status = None self._idle = QColor('#99FF99') self._busy = QColor(Qt.yellow) self.statusBar = QStatusBar(self) policy = self.statusBar.sizePolicy() policy.setVerticalPolicy(QSizePolicy.Fixed) self.statusBar.setSizePolicy(policy) self.statusBar.setSizeGripEnabled(False) self.layout().addWidget(self.statusBar) client.cache.connect(self.on_client_cache) run = self.buttonBox.button(QDialogButtonBox.Yes) run.setText('Run') run.setIcon(QIcon(':/continue')) self.buttonBox.accepted.connect(self.on_start_clicked) def saveSettings(self, settings): settings.setValue('geometry', self.saveGeometry()) def updateStatus(self, status, exception=False): self.current_status = status setBackgroundColor(self.curstatus, self._idle if status == 'idle' else self._busy) def on_client_cache(self, data): _time, key, _op, value = data if key == 'exp/action': self.curstatus.setText(cache_load(value) or 'Idle') @pyqtSlot() def on_start_clicked(self): dpos = [] for dp, cb in zip( [1, 2, 5, 10, 20], [self.dp1m, self.dp2m, self.dp5m, self.dp10m, self.dp20m]): if cb.isChecked(): dpos.append(dp) if not dpos: self.showInfo('Select at least one detector position!') return ctime = self.ctime.value() coll = self.coll10.isChecked() and '10m' or \ (self.coll15.isChecked() and '15m' or '20m') code = 'maw(coll, %r)\nscan(det1_z, [%s], det, t=%.1f)\n' % \ (coll, ', '.join(str(x) for x in dpos), ctime) self.execScript(code) def execScript(self, script): action = 'queue' if self.current_status != 'idle': qwindow = dialogFromUi(self, 'panels/question.ui') qwindow.questionText.setText('A script is currently running. What' ' do you want to do?') icon = qwindow.style().standardIcon qwindow.iconLabel.setPixmap( icon(QStyle.SP_MessageBoxQuestion).pixmap(32, 32)) b0 = QPushButton(icon(QStyle.SP_DialogCancelButton), 'Cancel') b1 = QPushButton(icon(QStyle.SP_DialogOkButton), 'Queue script') b2 = QPushButton(icon(QStyle.SP_MessageBoxWarning), 'Execute now!') qwindow.buttonBox.addButton(b0, QDialogButtonBox.ApplyRole) qwindow.buttonBox.addButton(b1, QDialogButtonBox.ApplyRole) qwindow.buttonBox.addButton(b2, QDialogButtonBox.ApplyRole) qwindow.buttonBox.setFocus() result = [0] def pushed(btn): if btn is b1: result[0] = 1 elif btn is b2: result[0] = 2 qwindow.accept() qwindow.buttonBox.clicked.connect(pushed) qwindow.exec_() if result[0] == 0: return elif result[0] == 2: action = 'execute' if action == 'queue': self.client.run(script) else: self.client.tell('exec', script)