class RunList(QtWidgets.QTreeWidget): """Shows the list of runs for a given date selection.""" cols = ['Run ID', 'Experiment', 'Sample', 'Name', 'Started', 'Completed', 'Records', 'GUID'] runSelected = Signal(int) runActivated = Signal(int) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.setColumnCount(len(self.cols)) self.setHeaderLabels(self.cols) self.itemSelectionChanged.connect(self.selectRun) self.itemActivated.connect(self.activateRun) def addRun(self, runId: int, **vals: str) -> None: lst = [str(runId)] lst.append(vals.get('experiment', '')) lst.append(vals.get('sample', '')) lst.append(vals.get('name', '')) lst.append(vals.get('started date', '') + ' ' + vals.get('started time', '')) lst.append(vals.get('completed date', '') + ' ' + vals.get('completed time', '')) lst.append(str(vals.get('records', ''))) lst.append(vals.get('guid', '')) item = SortableTreeWidgetItem(lst) self.addTopLevelItem(item) def setRuns(self, selection: Dict[int, Dict[str, str]]) -> None: self.clear() # disable sorting before inserting values to avoid performance hit self.setSortingEnabled(False) for runId, record in selection.items(): self.addRun(runId, **record) self.setSortingEnabled(True) for i in range(len(self.cols)): self.resizeColumnToContents(i) @Slot() def selectRun(self) -> None: selection = self.selectedItems() if len(selection) == 0: return runId = int(selection[0].text(0)) self.runSelected.emit(runId) @Slot(QtWidgets.QTreeWidgetItem, int) def activateRun(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None: runId = int(item.text(0)) self.runActivated.emit(runId)
class NumberInput(QtWidgets.QLineEdit): """A text edit widget that checks whether its input can be read as a number. This is copied form the parameter GUI that Wolfgang wrote for the parameter manager gui. """ newTextEntered = Signal(str) def __init__(self, default_value: Union[numbers.Number, None], parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.setValue(default_value) self.editingFinished.connect(self.emitNewText) def value(self) -> Optional[numbers.Number]: try: value = eval(self.text()) except: return None if isinstance(value, numbers.Number): return value else: return None def setValue(self, value: Union[numbers.Number, None]) -> None: self.setText(str(value)) def emitNewText(self) -> None: self.newTextEntered.emit(self.text())
class MonitorIntervalInput(QtWidgets.QWidget): """ Simple form-like widget for entering a monitor/refresh interval. Only has a label and a spin-box as input. It's signal `intervalChanged(float)' is emitted when the value of the spinbox has changed. """ intervalChanged = Signal(float) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.spin = QtWidgets.QDoubleSpinBox() self.spin.setSingleStep(0.1) self.spin.setDecimals(1) layout = QtWidgets.QFormLayout() layout.addRow('Refresh interval (s)', self.spin) self.setLayout(layout) self.spin.valueChanged.connect(self.spinValueChanged) @Slot(float) def spinValueChanged(self, val: float) -> None: self.intervalChanged.emit(val)
class ScaleUnitOptionWidget(QtWidgets.QWidget): """A widget that allows the user to specify if units should be scaled.""" unit_scale_selected = Signal(ScaleUnitsOption) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.buttons = { ScaleUnitsOption.never: QtWidgets.QRadioButton('Never'), ScaleUnitsOption.always: QtWidgets.QRadioButton('Always'), } btnLayout = QtWidgets.QVBoxLayout() self.btnGroup = QtWidgets.QButtonGroup(self) for opt in ScaleUnitsOption: btn = self.buttons[opt] self.btnGroup.addButton(btn, opt.value) btnLayout.addWidget(btn) layout = QtWidgets.QVBoxLayout() layout.addLayout(btnLayout) layout.addStretch() self.setLayout(layout) self.buttons[ScaleUnitsOption.always].setChecked(True) self.btnGroup.buttonToggled.connect(self.unitscale_button_selected) @Slot(QtWidgets.QAbstractButton, bool) def unitscale_button_selected(self, btn: QtWidgets.QAbstractButton, checked: bool) -> None: if checked: self.unit_scale_selected.emit( ScaleUnitsOption(self.btnGroup.id(btn)))
class _Loader(QtCore.QObject): nRetries = 5 retryDelay = 0.01 dataLoaded = Signal(object) def __init__(self, filepath: Optional[str], groupname: Optional[str]) -> None: super().__init__() self.filepath = filepath self.groupname = groupname def setPathAndGroup(self, filepath: Optional[str], groupname: Optional[str]) -> None: self.filepath = filepath self.groupname = groupname def loadData(self) -> bool: if self.filepath is None or self.groupname is None: self.dataLoaded.emit(None) return True try: data = datadict_from_hdf5(self.filepath, groupname=self.groupname, n_retries=self.nRetries, retry_delay=self.retryDelay) self.dataLoaded.emit(data) except OSError: self.dataLoaded.emit(None) return True
class LoadDBProcess(QtCore.QObject): """ Worker object for getting a qcodes db overview as pandas dataframe. It's good to have this in a separate thread because it can be a bit slow for large databases. """ dbdfLoaded = Signal(object) pathSet = Signal() def setPath(self, path: str) -> None: self.path = path self.pathSet.emit() def loadDB(self) -> None: dbdf = get_runs_from_db_as_dataframe(self.path) self.dbdfLoaded.emit(dbdf)
class DimensionCombo(QtGui.QComboBox): dimensionSelected = Signal(str) def __init__(self, parent=None, dimensionType='axes'): super().__init__(parent) self.node = None self.dimensionType = dimensionType self.clear() self.entries = ['None'] for e in self.entries: self.addItem(e) self.currentTextChanged.connect(self.signalDimensionSelection) def connectNode(self, node: Node = None): self.node = node if self.dimensionType == 'axes': self.node.dataAxesChanged.connect(self.setDimensions) else: raise NotImplementedError('Only Axes supported ATM.') @updateGuiQuietly def setDimensions(self, dims: List[str]): self.clear() allDims = self.entries + dims for d in allDims: self.addItem(d) @Slot(str) @emitGuiUpdate('dimensionSelected') def signalDimensionSelection(self, val: str): return val
class FigureConfigToolBar(QtWidgets.QToolBar): """Simple toolbar to configure the figure.""" # TODO: find better config system that generates GUI automatically and # links updates easier. #: Signal() -- emitted when options have been changed in the GUI. optionsChanged = Signal() def __init__(self, options: FigureOptions, parent: Optional[QtWidgets.QWidget] = None) -> None: """Constructor. :param options: options object. GUI interaction will make changes in-place to this object. :param parent: parent Widget """ super().__init__(parent) self.options = options combineLinePlots = self.addAction("Combine 1D") combineLinePlots.setCheckable(True) combineLinePlots.setChecked(self.options.combineLinePlots) combineLinePlots.triggered.connect( lambda: self._setOption('combineLinePlots', combineLinePlots.isChecked()) ) complexOptions = QtWidgets.QMenu(parent=self) complexGroup = QtWidgets.QActionGroup(complexOptions) complexGroup.setExclusive(True) for k in ComplexRepresentation: a = QtWidgets.QAction(k.label, complexOptions) a.setCheckable(True) complexGroup.addAction(a) complexOptions.addAction(a) a.setChecked(k == self.options.complexRepresentation) complexGroup.triggered.connect( lambda _a: self._setOption('complexRepresentation', ComplexRepresentation.fromLabel(_a.text())) ) complexButton = QtWidgets.QToolButton() complexButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly) complexButton.setText('Complex') complexButton.setPopupMode(QtWidgets.QToolButton.InstantPopup) complexButton.setMenu(complexOptions) self.addWidget(complexButton) def _setOption(self, option: str, value: Any) -> None: setattr(self.options, option, value) self.optionsChanged.emit()
class DataSource(QtCore.QObject): """Abstract data source. For specific data, implement a child class.""" dataready = Signal(object) nomoredata = Signal() initialdelay: float = 1.0 delay: float = 0.0 def data(self) -> Iterable[DataDictBase]: raise NotImplementedError def gimmesomedata(self) -> None: _nsets = 0 sleep(self.initialdelay) _t0 = time() logger.info("DataSource: start producing data.") for d in self.data(): logger.info(f"DataSource: producing set {_nsets}") self.dataready.emit(d) _nsets += 1 sleep(self.delay) logger.info(f"DataSource: Finished production after {time() - _t0} s") self.nomoredata.emit()
class DimensionCombo(QtWidgets.QComboBox): dimensionSelected = Signal(str) def __init__(self, parent: Optional[QtWidgets.QWidget] = None, dimensionType: str = 'axes'): super().__init__(parent) self.node: Optional[Node] = None self.dimensionType = dimensionType self.clear() self.entries = ['None'] for e in self.entries: self.addItem(e) self.currentTextChanged.connect(self.signalDimensionSelection) def connectNode(self, node: Optional[Node] = None) -> None: if node is None: raise RuntimeError self.node = node if self.dimensionType == 'axes': self.node.dataAxesChanged.connect(self.setDimensions) else: raise NotImplementedError('Only Axes supported ATM.') @updateGuiQuietly def setDimensions(self, dims: Sequence[str]) -> None: self.clear() allDims = self.entries + list(dims) for d in allDims: self.addItem(d) @Slot(str) @emitGuiUpdate('dimensionSelected') def signalDimensionSelection(self, val: str) -> str: return val
class AutoPlotToolBar(QtWidgets.QToolBar): """ A toolbar that allows the user to configure AutoPlot. Currently, the user can select between the plots that are possible, given the data that AutoPlot has. """ #: signal emitted when the plot type has been changed plotTypeSelected = Signal(PlotType) #: signal emitted when the complex data option has been changed complexRepresentationSelected = Signal(ComplexRepresentation) def __init__(self, name: str, parent: Optional[QtWidgets.QWidget] = None): """Constructor for :class:`AutoPlotToolBar`""" super().__init__(name, parent=parent) self.plotasMultiTraces = self.addAction(get_multiTracePlotIcon(), 'Multiple traces') self.plotasMultiTraces.setCheckable(True) self.plotasMultiTraces.triggered.connect( lambda: self.selectPlotType(PlotType.multitraces)) self.plotasSingleTraces = self.addAction(get_singleTracePlotIcon(), 'Individual traces') self.plotasSingleTraces.setCheckable(True) self.plotasSingleTraces.triggered.connect( lambda: self.selectPlotType(PlotType.singletraces)) self.addSeparator() self.plotasImage = self.addAction(get_imagePlotIcon(), 'Image') self.plotasImage.setCheckable(True) self.plotasImage.triggered.connect( lambda: self.selectPlotType(PlotType.image)) self.plotasMesh = self.addAction(get_colormeshPlotIcon(), 'Color mesh') self.plotasMesh.setCheckable(True) self.plotasMesh.triggered.connect( lambda: self.selectPlotType(PlotType.colormesh)) self.plotasScatter2d = self.addAction(get_scatterPlot2dIcon(), 'Scatter 2D') self.plotasScatter2d.setCheckable(True) self.plotasScatter2d.triggered.connect( lambda: self.selectPlotType(PlotType.scatter2d)) # other options self.addSeparator() self.plotReal = self.addAction('Real') self.plotReal.setCheckable(True) self.plotReal.triggered.connect( lambda: self.selectComplexType(ComplexRepresentation.real)) self.plotReIm = self.addAction('Re/Im') self.plotReIm.setCheckable(True) self.plotReIm.triggered.connect( lambda: self.selectComplexType(ComplexRepresentation.realAndImag)) self.plotReImSep = self.addAction('Split Re/Im') self.plotReImSep.setCheckable(True) self.plotReImSep.triggered.connect(lambda: self.selectComplexType( ComplexRepresentation.realAndImagSeparate)) self.plotMagPhase = self.addAction('Mag/Phase') self.plotMagPhase.setCheckable(True) self.plotMagPhase.triggered.connect( lambda: self.selectComplexType(ComplexRepresentation.magAndPhase)) self.plotTypeActions = OrderedDict({ PlotType.multitraces: self.plotasMultiTraces, PlotType.singletraces: self.plotasSingleTraces, PlotType.image: self.plotasImage, PlotType.colormesh: self.plotasMesh, PlotType.scatter2d: self.plotasScatter2d, }) self.ComplexActions = OrderedDict({ ComplexRepresentation.real: self.plotReal, ComplexRepresentation.realAndImag: self.plotReIm, ComplexRepresentation.realAndImagSeparate: self.plotReImSep, ComplexRepresentation.magAndPhase: self.plotMagPhase }) self._currentPlotType = PlotType.empty self._currentlyAllowedPlotTypes: Tuple[PlotType, ...] = () self._currentComplex = ComplexRepresentation.realAndImag self.ComplexActions[self._currentComplex].setChecked(True) self._currentlyAllowedComplexTypes: Tuple[ComplexRepresentation, ...] = () def selectPlotType(self, plotType: PlotType) -> None: """makes sure that the selected `plotType` is active (checked), all others are not active. This method should be used to catch a trigger from the UI. If the active plot type has been changed by using this method, we emit `plotTypeSelected`. :param plotType: type of plot """ # deselect all other types for k, v in self.plotTypeActions.items(): if k is not plotType and v is not None: v.setChecked(False) # don't want un-toggling - can only be done by selecting another type self.plotTypeActions[plotType].setChecked(True) if plotType is not self._currentPlotType: self._currentPlotType = plotType self.plotTypeSelected.emit(plotType) def setAllowedPlotTypes(self, *args: PlotType) -> None: """Disable all plot type choices that are not allowed. If the current selection is now disabled, instead select the first enabled one. :param args: which types of plots can be selected. """ if args == self._currentlyAllowedPlotTypes: return for k, v in self.plotTypeActions.items(): if k not in args: v.setChecked(False) v.setEnabled(False) else: v.setEnabled(True) if self._currentPlotType not in args: self._currentPlotType = PlotType.empty for k, v in self.plotTypeActions.items(): if k in args: v.setChecked(True) self._currentPlotType = k break self.plotTypeSelected.emit(self._currentPlotType) self._currentlyAllowedPlotTypes = args def selectComplexType(self, comp: ComplexRepresentation) -> None: """makes sure that the selected `comp` is active (checked), all others are not active. This method should be used to catch a trigger from the UI. If the active plot type has been changed by using this method, we emit `complexPolarSelected`. """ # deselect all other types for k, v in self.ComplexActions.items(): if k is not comp and v is not None: v.setChecked(False) # don't want un-toggling - can only be done by selecting another type self.ComplexActions[comp].setChecked(True) if comp is not self._currentComplex: self._currentComplex = comp self.complexRepresentationSelected.emit(self._currentComplex) def setAllowedComplexTypes(self, *complexOptions: ComplexRepresentation) -> None: """Disable all complex representation choices that are not allowed. If the current selection is now disabled, instead select the first enabled one. """ if complexOptions == self._currentlyAllowedComplexTypes: return for k, v in self.ComplexActions.items(): if k not in complexOptions: v.setChecked(False) v.setEnabled(False) else: v.setEnabled(True) if self._currentComplex not in complexOptions: self._currentComplex = ComplexRepresentation.realAndImag for k, v in self.ComplexActions.items(): if k in complexOptions: v.setChecked(True) self._currentComplex = k break self.complexRepresentationSelected.emit(self._currentComplex) self._currentlyAllowedComplexTypes = complexOptions
class PlotWindow(QtWidgets.QMainWindow): """ Simple MainWindow class for embedding flowcharts and plots. All keyword arguments supplied will be propagated to :meth:`addNodeWidgetFromFlowchart`. """ #: Signal() -- emitted when the window is closed windowClosed = Signal() def __init__(self, parent: Optional[QtWidgets.QMainWindow] = None, fc: Optional[Flowchart] = None, plotWidgetClass: Optional[Any] = None, **kw: Any): super().__init__(parent) if plotWidgetClass is None: from ..plot.mpl import AutoPlot plotWidgetClass = AutoPlot self.plotWidgetClass = plotWidgetClass self.plot = PlotWidgetContainer(parent=self) self.setCentralWidget(self.plot) self.plotWidget: Optional[PlotWidget] = None self.nodeToolBar = QtWidgets.QToolBar('Node control', self) self.addToolBar(self.nodeToolBar) self.nodeWidgets: Dict[str, QtWidgets.QDockWidget] = {} if fc is not None: self.addNodeWidgetsFromFlowchart(fc, **kw) self.setDefaultStyle() def setDefaultStyle(self) -> None: fontSize = 10 * dpiScalingFactor(self) self.setStyleSheet(f""" QToolButton {{ font: {fontSize}px; }} QToolBar QCheckBox {{ font: {fontSize}px; }} """) def addNodeWidget(self, node: Node, **kwargs: Any) -> None: """ Add a node widget as dock. :param node: node for which to add the widget. :keyword arguments: * *visible* (`bool`; default: taken from widget class definition) -- whether the widget is visible from the start * *dockArea* (`QtCore.Qt.DockWidgetArea`; default: taken from class) -- where the dock widget initially sits in the window * *icon* (`QtCore.QIcon`; default: taken from class) -- an icon to use for the toolbar """ if node.useUi and node.ui is not None and node.uiClass is not None: dockArea = kwargs.get('dockArea', node.ui.preferredDockWidgetArea) visible = kwargs.get('visible', node.uiVisibleByDefault) icon = kwargs.get('icon', node.ui.icon) d = QtWidgets.QDockWidget(node.name(), self) d.setWidget(node.ui) self.nodeWidgets[node.name()] = d self.addDockWidget(dockArea, d) action = d.toggleViewAction() if icon is not None: action.setIcon(icon) self.nodeToolBar.addAction(action) if not visible: d.close() def addNodeWidgetsFromFlowchart(self, fc: Flowchart, exclude: Sequence[str] = (), plotNode: str = 'plot', makePlotWidget: bool = True, **kwargs: Any) -> None: """ Add all nodes for a flowchart, excluding nodes given in `exclude`. :param fc: flowchart object :param exclude: list of node names. 'Input' and 'Output' are automatically appended. :param plotNode: specify the name of the plot node, if present :param makePlotWidget: if True, attach a MPL autoplot widget to the plot node. :param kwargs: see below. :keyword arguments: * *widgetOptions* (`dictionary`) -- each entry in the dictionary should have the form { nodeName : { option : value, ...}, ... }. the options will be passed to :meth:`addNodeWidget` as keyword arguments. """ exclude = tuple(exclude) + ('Input', 'Output') opts = kwargs.get('widgetOptions', dict()) for nodeName, node in fc.nodes().items(): if nodeName not in exclude: thisOpts = opts.get(nodeName, dict()) self.addNodeWidget(node, **thisOpts) if nodeName == plotNode and makePlotWidget: pn = fc.nodes().get(plotNode, None) if pn is not None and isinstance(pn, PlotNode): pn.setPlotWidgetContainer(self.plot) self.plotWidget = self.plotWidgetClass(parent=self.plot) self.plot.setPlotWidget(self.plotWidget) def closeEvent(self, event: QtGui.QCloseEvent) -> None: """ When closing the inspectr window, do some house keeping: * stop the monitor, if running """ self.windowClosed.emit() return event.accept()
class RunList(QtWidgets.QTreeWidget): """Shows the list of runs for a given date selection.""" cols = [ 'Run ID', 'Experiment', 'Sample', 'Name', 'Started', 'Completed', 'Records', 'GUID' ] runSelected = Signal(int) runActivated = Signal(int) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.setColumnCount(len(self.cols)) self.setHeaderLabels(self.cols) self.itemSelectionChanged.connect(self.selectRun) self.itemActivated.connect(self.activateRun) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.copy_to_clipboard) @Slot(QtCore.QPoint) def copy_to_clipboard(self, position: QtCore.QPoint) -> None: menu = QtWidgets.QMenu() copy_icon = self.style().standardIcon( QtWidgets.QStyle.SP_DialogSaveButton) copy_action = menu.addAction(copy_icon, "Copy") action = menu.exec_(self.mapToGlobal(position)) if action == copy_action: model_index = self.indexAt(position) item = self.itemFromIndex(model_index) QtWidgets.QApplication.clipboard().setText( item.text(model_index.column())) def addRun(self, runId: int, **vals: str) -> None: lst = [str(runId)] lst.append(vals.get('experiment', '')) lst.append(vals.get('sample', '')) lst.append(vals.get('name', '')) lst.append( vals.get('started_date', '') + ' ' + vals.get('started_time', '')) lst.append( vals.get('completed_date', '') + ' ' + vals.get('completed_time', '')) lst.append(str(vals.get('records', ''))) lst.append(vals.get('guid', '')) item = SortableTreeWidgetItem(lst) self.addTopLevelItem(item) def setRuns(self, selection: Dict[int, Dict[str, str]]) -> None: self.clear() # disable sorting before inserting values to avoid performance hit self.setSortingEnabled(False) for runId, record in selection.items(): self.addRun(runId, **record) self.setSortingEnabled(True) for i in range(len(self.cols)): self.resizeColumnToContents(i) def updateRuns(self, selection: Dict[int, Dict[str, str]]) -> None: run_added = False for runId, record in selection.items(): item = self.findItems(str(runId), QtCore.Qt.MatchExactly) if len(item) == 0: self.setSortingEnabled(False) self.addRun(runId, **record) run_added = True elif len(item) == 1: completed = record.get('completed_date', '') + ' ' + record.get( 'completed_time', '') if completed != item[0].text(5): item[0].setText(5, completed) num_records = str(record.get('records', '')) if num_records != item[0].text(6): item[0].setText(6, num_records) else: raise RuntimeError(f"More than one runs found with runId: " f"{runId}") if run_added: self.setSortingEnabled(True) for i in range(len(self.cols)): self.resizeColumnToContents(i) @Slot() def selectRun(self) -> None: selection = self.selectedItems() if len(selection) == 0: return runId = int(selection[0].text(0)) self.runSelected.emit(runId) @Slot(QtWidgets.QTreeWidgetItem, int) def activateRun(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None: runId = int(item.text(0)) self.runActivated.emit(runId)
class ShapeSpecificationWidget(QtWidgets.QWidget): """A widget that allows the user to specify a grid shape. Note that this widget in this form knows nothing about any underlying data, and does not perform any checking of validity for submitted shapes. Such functionality would need to be implemented by users or inheriting classes. """ #: signal that is emitted when we want to communicate a new shape newShapeNotification = Signal(dict) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self._axes: List[str] = [] self._widgets: Dict[int, Dict[str, QtWidgets.QWidget]] = {} self._processChanges = True layout = QtWidgets.QFormLayout() self.confirm = QtWidgets.QPushButton('set') layout.addRow(self.confirm) self.setLayout(layout) self.confirm.clicked.connect(self.signalShape) def signalShape(self) -> None: """When called, emit the current shape as signal""" self.newShapeNotification.emit(self.getShape()) def _addAxis(self, idx: int, name: str) -> None: nameWidget = QtWidgets.QComboBox() for j, bx in enumerate(self._axes): nameWidget.addItem(bx) nameWidget.setCurrentText(name) dimLenWidget = QtWidgets.QSpinBox() dimLenWidget.setMinimum(1) dimLenWidget.setMaximum(999999) self._widgets[idx] = { 'name': nameWidget, 'shape': dimLenWidget, } self.layout().insertRow(idx, nameWidget, dimLenWidget) nameWidget.currentTextChanged.connect( lambda x: self._processAxisChange(idx, x) ) def setAxes(self, axes: List[str]) -> None: """Specify a set of axis dimensions If the axes do not match the previous ones, delete all widgets and recreate. """ if axes != self._axes: self._axes = axes for i in range(self.layout().rowCount() - 1): self._widgets[i]['name'].deleteLater() self._widgets[i]['shape'].deleteLater() self.layout().removeRow(0) self._widgets = {} for i, ax in enumerate(axes): self._addAxis(i, ax) def _unusedAxes(self) -> List[str]: names = self._axes.copy() for k, v in self._widgets.items(): ax = v['name'].currentText() if ax in names: del names[names.index(ax)] return names def _axisIndexFromName(self, name: str, excludeIdxs: Sequence[int] = ()) -> Optional[int]: for k, v in self._widgets.items(): if k not in excludeIdxs and v['name'].currentText() == name: return k return None def _processAxisChange(self, idx: int, newName: str) -> None: if not self._processChanges: return prevIdx = self._axisIndexFromName(newName, excludeIdxs=[idx]) unused = self._unusedAxes() if prevIdx is not None and len(unused) > 0: self._processChanges = False self._widgets[prevIdx]['name'].setCurrentText(unused[0]) self._processChanges = True def setShape(self, shape: Dict[str, Tuple[Union[str, int], ...]]) -> None: """ Set the shape, will be reflected in the values set in the widgets. :param shape: A dictionary with keys `order` and `shape`. The value of `order` must be a tuple with the axes names, ordered as desired. The value of `shape` is a tuple with the size of each axis dimension, in the order given by `order`. """ if 'order' in shape and 'shape' in shape: self._processChanges = False for i, (o, s) in enumerate(zip(shape['order'], shape['shape'])): self._widgets[i]['name'].setCurrentText(o) self._widgets[i]['shape'].setValue(s) self._processChanges = True def getShape(self) -> Dict[str, Tuple[Union[str, int], ...]]: """get the currently specified shape. :returns: a dictionary with keys `order` and `shape`. the `order` value is a tuple with the axis names in order, and the `shape` value is the shape tuple of the grid, in the order as specified in the `order` value. """ order = [] shape = [] for k, v in self._widgets.items(): order.append(v['name'].currentText()) shape.append(v['shape'].value()) return {'order': tuple(order), 'shape': tuple(shape)} def enableEditing(self, enable: bool) -> None: for ax, widgets in self._widgets.items(): widgets['name'].setEnabled(enable) widgets['shape'].setEnabled(enable) self.confirm.setEnabled(enable)
class DataFileList(QtWidgets.QTreeWidget): """A Tree Widget that displays all data files that are in a certain base directory. All subfolders are monitored. """ fileExtensions = ['.ddh5'] #: Signal(str) -- emitted when a data file is selected #: Arguments: #: - the absolute path of the data file dataFileSelected = Signal(str) #: Signal(list) -- emitted when new files have been found newDataFilesFound = Signal(list) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.files: List[str] = [] self.path: Optional[str] = None @staticmethod def finditem(parent: Union["DataFileList", QtWidgets.QTreeWidgetItem], name: str) -> Optional[QtWidgets.QTreeWidgetItem]: if isinstance(parent, DataFileList): existingItems = [ parent.topLevelItem(i) for i in range(parent.topLevelItemCount()) ] else: existingItems = [ parent.child(i) for i in range(parent.childCount()) ] item = None for item_ in existingItems: if item_ is not None: if item_.text(0) == name: item = item_ break return item def itemPath(self, item: QtWidgets.QTreeWidgetItem) -> str: def buildPath(i: Optional[QtWidgets.QTreeWidgetItem], suffix: str = '') -> str: if i is None: return suffix newSuffix = i.text(0) if suffix != '': newSuffix += os.path.sep + suffix return buildPath(i.parent(), suffix=newSuffix) assert self.path is not None return os.path.join(self.path, buildPath(item)) def findItemByPath(self, path: str) -> Optional[QtWidgets.QTreeWidgetItem]: assert self.path is not None path = path[len(self.path) + len(os.path.sep):] pathList = path.split(os.path.sep) parent: Union["DataFileList", QtWidgets.QTreeWidgetItem] = self for p in pathList: new_parent = self.finditem(parent, p) if new_parent is None: return None else: parent = new_parent assert isinstance(parent, QtWidgets.QTreeWidgetItem) return parent def addItemByPath(self, path: str) -> None: assert self.path is not None path = path[len(self.path) + len(os.path.sep):] pathList = path.split(os.path.sep) def add(parent: Union["DataFileList", QtWidgets.QTreeWidgetItem], name: str) -> Union["DataFileList", QtWidgets.QTreeWidgetItem]: item = self.finditem(parent, name) if item is None: item = QtWidgets.QTreeWidgetItem(parent, [name]) if os.path.splitext(name)[-1] in self.fileExtensions: fnt = QtGui.QFont() item.setFont(0, fnt) else: pass if isinstance(parent, DataFileList): parent.addTopLevelItem(item) else: parent.addChild(item) return item parent: Union["DataFileList", QtWidgets.QTreeWidgetItem] = self for p in pathList: parent = add(parent, p) def removeItemByPath(self, path: str) -> None: def remove(i: QtWidgets.QTreeWidgetItem) -> None: parent = i.parent() if isinstance(parent, DataFileList): idx = parent.indexOfTopLevelItem(i) parent.takeTopLevelItem(idx) elif isinstance(parent, QtWidgets.QTreeWidgetItem): parent.removeChild(i) if parent.childCount() == 0: remove(parent) item = self.findItemByPath(path) if item is None: return remove(item) def loadFromPath(self, path: str, emitNew: bool = False) -> None: self.path = path files = findFilesByExtension(path, self.fileExtensions) newFiles = [f for f in files if f not in self.files] removedFiles = [f for f in self.files if f not in files] for f in newFiles: self.addItemByPath(f) for f in removedFiles: self.removeItemByPath(f) self.files = files if len(newFiles) > 0 and emitNew: self.newDataFilesFound.emit(newFiles) @Slot() def processSelection(self) -> None: selected = self.selectedItems() if len(selected) == 0: return nameAndExt = os.path.splitext(selected[0].text(0)) if nameAndExt[-1] in self.fileExtensions: path = self.itemPath(selected[0]) self.dataFileSelected.emit(path)
class DataFileContent(QtWidgets.QTreeWidget): #: Signal(str) -- Emitted when the user requests a plot for datadict #: Arguments: #: - name of the group within the currently selected file plotRequested = Signal(str) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.data: Dict[str, DataDict] = {} self.groupItems: List[QtWidgets.QTreeWidgetItem] = [] self.selectedGroup = None self.dataPopup = QtWidgets.QMenu('Data actions', self) self.plotAction = self.dataPopup.addAction("Plot") self.plotAction.triggered.connect(self.onPlotActionTriggered) @Slot(object) def setData(self, data: Dict[str, DataDict]) -> None: """Set the data to display.""" self.clear() self.data = {} self.groupItems = [] for grpName, grpData in data.items(): self.data[grpName] = data[grpName] grpItem = QtWidgets.QTreeWidgetItem(self, [grpName]) self.groupItems.append(grpItem) self.addTopLevelItem(grpItem) dataParent = QtWidgets.QTreeWidgetItem(grpItem, ['[DATA]']) metaParent = QtWidgets.QTreeWidgetItem(grpItem, ['[META]']) for dn, dv in grpData.data_items(): label = grpData.label(dn) assert label is not None vals = [label, str(grpData.meta_val('shape', dn))] if dn in grpData.dependents(): vals.append( f'Data (depends on {str(tuple(grpData.axes(dn)))[1:]}') else: vals.append('Data (independent)') ditem = QtWidgets.QTreeWidgetItem(dataParent, vals) for mn, mv in grpData.meta_items(dn): vals = [mn, str(mv)] _ = QtWidgets.QTreeWidgetItem(ditem, vals) for mn, mv in grpData.meta_items(): vals = [mn, str(mv)] _ = QtWidgets.QTreeWidgetItem(metaParent, vals) grpItem.setExpanded(True) dataParent.setExpanded(True) for i in range(self.columnCount() - 1): self.resizeColumnToContents(i) @Slot(QtCore.QPoint) def onCustomContextMenuRequested(self, pos: QtCore.QPoint) -> None: item = self.itemAt(pos) if item not in self.groupItems: return self.selectedGroup = item.text(0) self.plotAction.setText(f"Plot '{item.text(0)}'") self.dataPopup.exec(self.mapToGlobal(pos)) @Slot() def onPlotActionTriggered(self) -> None: self.plotRequested.emit(self.selectedGroup)
class DateList(QtWidgets.QListWidget): """Displays a list of dates for which there are runs in the database.""" datesSelected = Signal(list) fileDropped = Signal(str) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.setAcceptDrops(True) self.setDefaultDropAction(QtCore.Qt.CopyAction) self.setSelectionMode(QtWidgets.QListView.ExtendedSelection) self.itemSelectionChanged.connect(self.sendSelectedDates) @Slot(list) def updateDates(self, dates: Sequence[str]) -> None: for d in dates: if len(self.findItems(d, QtCore.Qt.MatchExactly)) == 0: self.insertItem(0, d) i = 0 while i < self.count(): if self.item(i).text() not in dates: item = self.takeItem(i) del item else: i += 1 if i >= self.count(): break self.sortItems(QtCore.Qt.DescendingOrder) @Slot() def sendSelectedDates(self) -> None: selection = [item.text() for item in self.selectedItems()] self.datesSelected.emit(selection) ### Drag/drop handling def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: if event.mimeData().hasUrls(): urls = event.mimeData().urls() if len(urls) == 1: url = urls[0] if url.isLocalFile(): event.accept() else: event.ignore() else: event.ignore() def dropEvent(self, event: QtGui.QDropEvent) -> None: url = event.mimeData().urls()[0].toLocalFile() self.fileDropped.emit(url) def mimeTypes(self) -> List[str]: return ([ 'text/uri-list', 'application/x-qabstractitemmodeldatalist', ])
class QCodesDBInspector(QtWidgets.QMainWindow): """ Main window of the inspectr tool. """ #: `Signal ()` -- Emitted when when there's an update to the internally #: cached data (the *data base data frame* :)). dbdfUpdated = Signal() #: Signal (`dict`) -- emitted to communicate information about a given #: run to the widget that displays the information _sendInfo = Signal(dict) def __init__(self, parent: Optional[QtWidgets.QWidget] = None, dbPath: Optional[str] = None): """Constructor for :class:`QCodesDBInspector`.""" super().__init__(parent) self._plotWindows: Dict[int, WindowDict] = {} self.filepath = dbPath self.dbdf = None self.monitor = QtCore.QTimer() # flag for determining what has been loaded so far. # * None: nothing opened yet. # * -1: empty DS open. # * any value > 0: run ID from the most recent loading. self.latestRunId = None self.setWindowTitle('Plottr | QCoDeS dataset inspectr') ### GUI elements # Main Selection widgets self.dateList = DateList() self._selected_dates: Tuple[str, ...] = () self.runList = RunList() self.runInfo = RunInfo() rightSplitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) rightSplitter.addWidget(self.runList) rightSplitter.addWidget(self.runInfo) rightSplitter.setSizes([400, 200]) splitter = QtWidgets.QSplitter() splitter.addWidget(self.dateList) splitter.addWidget(rightSplitter) splitter.setSizes([100, 500]) self.setCentralWidget(splitter) # status bar self.status = QtWidgets.QStatusBar() self.setStatusBar(self.status) # toolbar self.toolbar = self.addToolBar('Data monitoring') # toolbar item: monitor interval self.monitorInput = MonitorIntervalInput() self.monitorInput.setToolTip('Set to 0 for disabling') self.monitorInput.intervalChanged.connect(self.setMonitorInterval) self.toolbar.addWidget(self.monitorInput) self.toolbar.addSeparator() # toolbar item: auto-launch plotting self.autoLaunchPlots = FormLayoutWrapper([('Auto-plot new', QtWidgets.QCheckBox())]) tt = "If checked, and automatic refresh is running, " tt += " launch plotting window for new datasets automatically." self.autoLaunchPlots.setToolTip(tt) self.toolbar.addWidget(self.autoLaunchPlots) # menu bar menu = self.menuBar() fileMenu = menu.addMenu('&File') # action: load db file loadAction = QtWidgets.QAction('&Load', self) loadAction.setShortcut('Ctrl+L') loadAction.triggered.connect(self.loadDB) fileMenu.addAction(loadAction) # action: updates from the db file refreshAction = QtWidgets.QAction('&Refresh', self) refreshAction.setShortcut('R') refreshAction.triggered.connect(self.refreshDB) fileMenu.addAction(refreshAction) # sizing scaledSize = 640 * rint(self.logicalDpiX() / 96.0) self.resize(scaledSize, scaledSize) ### Thread workers # DB loading. can be slow, so nice to have in a thread. self.loadDBProcess = LoadDBProcess() self.loadDBThread = QtCore.QThread() self.loadDBProcess.moveToThread(self.loadDBThread) self.loadDBProcess.pathSet.connect(self.loadDBThread.start) self.loadDBProcess.dbdfLoaded.connect(self.DBLoaded) self.loadDBProcess.dbdfLoaded.connect(self.loadDBThread.quit) self.loadDBThread.started.connect( self.loadDBProcess.loadDB) # type: ignore[attr-defined] ### connect signals/slots self.dbdfUpdated.connect(self.updateDates) self.dbdfUpdated.connect(self.showDBPath) self.dateList.datesSelected.connect(self.setDateSelection) self.dateList.fileDropped.connect(self.loadFullDB) self.runList.runSelected.connect(self.setRunSelection) self.runList.runActivated.connect(self.plotRun) self._sendInfo.connect(self.runInfo.setInfo) self.monitor.timeout.connect(self.monitorTriggered) if self.filepath is not None: self.loadFullDB(self.filepath) def closeEvent(self, event: QtGui.QCloseEvent) -> None: """ When closing the inspectr window, do some house keeping: * stop the monitor, if running * close all plot windows """ if self.monitor.isActive(): self.monitor.stop() for runId, info in self._plotWindows.items(): info['window'].close() @Slot() def showDBPath(self) -> None: tstamp = time.strftime("%Y-%m-%d %H:%M:%S") assert self.filepath is not None path = os.path.abspath(self.filepath) self.status.showMessage(f"{path} (loaded: {tstamp})") ### loading the DB and populating the widgets @Slot() def loadDB(self) -> None: """ Open a file dialog that allows selecting a .db file for loading. If a file is selected, opens the db. """ if self.filepath is not None: curdir = os.path.split(self.filepath)[0] else: curdir = os.getcwd() path, _fltr = QtWidgets.QFileDialog.getOpenFileName( self, 'Open qcodes .db file', curdir, 'qcodes .db files (*.db);;all files (*.*)', ) if path: logger().info(f"Opening: {path}") self.loadFullDB(path=path) def loadFullDB(self, path: Optional[str] = None) -> None: if path is not None and path != self.filepath: self.filepath = path # makes sure we treat a newly loaded file fresh and not as a # refreshed one. self.latestRunId = None if self.filepath is not None: if not self.loadDBThread.isRunning(): self.loadDBProcess.setPath(self.filepath) def DBLoaded(self, dbdf: pandas.DataFrame) -> None: if dbdf.equals(self.dbdf): logger().debug('DB reloaded with no changes. Skipping update') return None self.dbdf = dbdf self.dbdfUpdated.emit() self.dateList.sendSelectedDates() logger().debug('DB reloaded') if self.latestRunId is not None: idxs = self.dbdf.index.values newIdxs = idxs[idxs > self.latestRunId] if self.monitor.isActive( ) and self.autoLaunchPlots.elements['Auto-plot new'].isChecked(): for idx in newIdxs: self.plotRun(idx) self._plotWindows[idx]['window'].setMonitorInterval( self.monitorInput.spin.value()) @Slot() def updateDates(self) -> None: assert self.dbdf is not None if self.dbdf.size > 0: dates = list(self.dbdf.groupby('started_date').indices.keys()) self.dateList.updateDates(dates) ### reloading the db @Slot() def refreshDB(self) -> None: if self.filepath is not None: if self.dbdf is not None and self.dbdf.size > 0: self.latestRunId = self.dbdf.index.values.max() else: self.latestRunId = -1 self.loadFullDB() @Slot(float) def setMonitorInterval(self, val: float) -> None: self.monitor.stop() if val > 0: self.monitor.start(int(val * 1000)) self.monitorInput.spin.setValue(val) @Slot() def monitorTriggered(self) -> None: logger().debug('Refreshing DB') self.refreshDB() ### handling user selections @Slot(list) def setDateSelection(self, dates: Sequence[str]) -> None: if len(dates) > 0: assert self.dbdf is not None selection = self.dbdf.loc[self.dbdf['started_date'].isin( dates)].sort_index(ascending=False) old_dates = self._selected_dates if not all(date in old_dates for date in dates): self.runList.setRuns(selection.to_dict(orient='index')) else: self.runList.updateRuns(selection.to_dict(orient='index')) self._selected_dates = tuple(dates) else: self._selected_dates = () self.runList.clear() @Slot(int) def setRunSelection(self, runId: int) -> None: assert self.filepath is not None ds = load_dataset_from(self.filepath, runId) snap = None if hasattr(ds, 'snapshot'): snap = ds.snapshot structure = cast(Dict[str, dict], get_ds_structure(ds)) # cast away typed dict so we can pop a key for k, v in structure.items(): v.pop('values') contentInfo = {'Data structure': structure, 'QCoDeS Snapshot': snap} self._sendInfo.emit(contentInfo) @Slot(int) def plotRun(self, runId: int) -> None: assert self.filepath is not None fc, win = autoplotQcodesDataset(pathAndId=(self.filepath, runId)) self._plotWindows[runId] = { 'flowchart': fc, 'window': win, } win.showTime()
class GridOptionWidget(QtWidgets.QWidget): """A widget that allows the user to specify how to grid data.""" optionSelected = Signal(object) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self._emitUpdate = True # make radio buttons and layout self.buttons = { GridOption.noGrid: QtWidgets.QRadioButton('No grid'), GridOption.guessShape: QtWidgets.QRadioButton('Guess shape'), GridOption.specifyShape: QtWidgets.QRadioButton('Specify shape'), GridOption.metadataShape: QtWidgets.QRadioButton( 'Read shape from metadata'), } btnLayout = QtWidgets.QVBoxLayout() self.btnGroup = QtWidgets.QButtonGroup(self) for opt in GridOption: btn = self.buttons[opt] self.btnGroup.addButton(btn, opt.value) btnLayout.addWidget(btn) # make shape spec widget self.shapeSpec = ShapeSpecificationWidget() shapeLayout = QtWidgets.QVBoxLayout() shapeLayout.addWidget(self.shapeSpec) shapeBox = QtWidgets.QGroupBox() shapeBox.setLayout(shapeLayout) # Widget layout layout = QtWidgets.QVBoxLayout() layout.addLayout(btnLayout) layout.addWidget(shapeBox) layout.addStretch() self.setLayout(layout) # Connect signals/slots # self.btnGroup.buttonToggled.connect(self.gridButtonSelected) self.shapeSpec.confirm.clicked.connect(self.shapeSpecified) # Default settings self.buttons[GridOption.noGrid].setChecked(True) self.enableShapeEdit(False) def getGrid(self) -> Tuple[GridOption, Dict[str, Any]]: """Get grid option from the current widget selections :returns: the grid specification, and the options that go with it. options are empty unless the grid specification is :mem:`GridOption.specifyShape`. In that case the additional options are `order` and `shape` as returned by :mem:`getShape`. """ activeBtn = self.btnGroup.checkedButton() activeId = self.btnGroup.id(activeBtn) opts = {} if GridOption(activeId) == GridOption.specifyShape: opts = self.shapeSpec.getShape() return GridOption(activeId), opts def setGrid(self, grid: Tuple[GridOption, Dict[str, Any]]) -> None: """Set the grid specification in the UI. :param grid: Tuple of the :class:`GridOption` and additional options. if `specifyShape` is the selection option, additional options need to be `order` and `shape`. """ # This function should not trigger an emission for an update. # We only want that when the user sets the grid in the UI, # to avoid recursive calls self._emitUpdate = False method, opts = grid for k, btn in self.buttons.items(): if k == method: btn.setChecked(True) self._emitUpdate = True @Slot(QtWidgets.QAbstractButton, bool) def gridButtonSelected(self, btn: QtWidgets.QAbstractButton, checked: bool) -> None: """Process a change in grid option radio box selection. Only has an effect when the change was done manually, and is not coming from the node. Will result in emission of :mem:`optionSelected` and enable/disable the shape specification widget depending on the new selection. """ if checked: # only emit the signal when the update is from the UI if self._emitUpdate: self.signalGridOption(self.getGrid()) if GridOption(self.btnGroup.id(btn)) == GridOption.specifyShape: self.enableShapeEdit(True) else: self.enableShapeEdit(False) self._emitUpdate = True @Slot() def shapeSpecified(self) -> None: self.signalGridOption(self.getGrid()) def signalGridOption(self, grid: Tuple[GridOption, Dict[str, Any]]) -> None: self.optionSelected.emit(grid) def setAxes(self, axes: List[str]) -> None: """Set the available axis dimensions.""" self.shapeSpec.setAxes(axes) if self.getGrid()[0] == GridOption.specifyShape: self.enableShapeEdit(True) else: self.enableShapeEdit(False) def setShape(self, shape: SpecShapeType) -> None: """Set the shape of the grid.""" self.shapeSpec.setShape(shape) def enableShapeEdit(self, enable: bool) -> None: """Enable/disable shape editing""" self.shapeSpec.enableEditing(enable)
class DataGridder(Node): """ A node that can put data onto or off a grid. Has one property: :attr:`grid`. Its possible values are governed by a main option, plus (optional) additional options. """ nodeName = "Gridder" uiClass = DataGridderNodeWidget #: signal emitted when we have programatically determined a shape for the data. shapeDetermined = Signal(dict) axesList = Signal(list) def __init__(self, name: str): self._grid: Tuple[GridOption, Dict[str, Any]] = (GridOption.noGrid, {}) self._shape = None self._invalid = False super().__init__(name) # Properties @property def grid(self) -> Tuple[GridOption, Dict[str, Any]]: """Specification for how to grid the data. Consists of a main option and (optional) additional options. The main option is of type :class:`GridOption`, and the additional options are given as a dictionary. Assign as tuple, like:: >>> dataGridder.grid = GridOption.<option>, dict((**options) All types of :class:`GridOption` are valid main options: * :attr:`GridOption.noGrid` -- will leave tabular data as is, and flatten gridded data to result in tabular data * :attr:`GridOption.guessShape` -- use :func:`.guess_shape_from_datadict` and :func:`.datadict_to_meshgrid` to infer the grid, if the input data is tabular. * :attr:`GridOption.specifyShape` -- reshape the data using a specified shape. * :attr:`GridOption.metadataShape` -- use the shape specified in the dataset metadata Some types may required additional options. At the moment, this is only the case for :attr:`GridOption.specifyShape`. Manual specification of the shape requires two additional options, `order` and `shape`: * `order` -- a list of the input data axis dimension names, in the internal order of the input data array. This order is used to transpose the data before re-shaping with the `shape` information. Often this is simply the axes list; then the transpose has no effect. A different order needed when the the data to be gridded is not in `C` order, i.e., when the axes order given in the DataDict is not from slowest changing to fastest changing. * `shape` -- a tuple of integers that can be used to reshape the input data to obtain a grid. Must be in the same order as `order` to work correctly. See :func:`.data.datadict.datadict_to_meshgrid` for additional notes; `order` will be passed to `inner_axis_order` in that function, and `shape` to `target_shape`. """ return self._grid @grid.setter # type: ignore[misc] @updateOption('grid') def grid(self, val: Tuple[GridOption, Dict[str, Any]]) -> None: """set the grid option. does some elementary type checking, but should probably be refined a bit.""" try: method, opts = val except TypeError: raise ValueError(f"Invalid grid specification.") if method not in GridOption: raise ValueError(f"Invalid grid method specification.") if not isinstance(opts, dict): raise ValueError(f"Invalid grid options specification.") self._grid = val # Processing def validateOptions(self, data: Any) -> bool: """Currently, does not perform checks beyond those of the parent class. """ if not super().validateOptions(data): return False return True def process( self, dataIn: Optional[DataDictBase] = None ) -> Optional[Dict[str, Optional[DataDictBase]]]: """Process the data.""" # TODO: what would be nice is to change the correct inner axis order # in the widget when we guess the shape. unfortunately, we currently # don't get that information from the guess function, and it is also # not reflected in the resulting data. if dataIn is None: return None data = super().process(dataIn=dataIn) if data is None: return None dataout = data['dataOut'] assert dataout is not None data = dataout.copy() self.axesList.emit(data.axes()) dout: Optional[DataDictBase] = None method, opts = self._grid order = opts.get('order', data.axes()) if isinstance(data, DataDict): if method is GridOption.noGrid: dout = data.expand() elif method is GridOption.guessShape: dout = dd.datadict_to_meshgrid(data) elif method is GridOption.specifyShape: dout = dd.datadict_to_meshgrid( data, target_shape=opts['shape'], inner_axis_order=order, ) elif method is GridOption.metadataShape: dout = dd.datadict_to_meshgrid( data, use_existing_shape=True ) elif isinstance(data, MeshgridDataDict): if method is GridOption.noGrid: dout = dd.meshgrid_to_datadict(data) elif method is GridOption.guessShape: dout = data elif method is GridOption.specifyShape: self.logger().warning( f"Data is already on grid. Ignore shape.") dout = data elif method is GridOption.metadataShape: self.logger().warning( f"Data is already on grid. Ignore shape.") dout = data else: self.logger().error( f"Unknown data type {type(data)}.") return None if dout is None: return None if hasattr(dout, 'shape'): assert isinstance(dout, MeshgridDataDict) self.shapeDetermined.emit({'order': order, 'shape': dout.shape()}) return dict(dataOut=dout) # Setup UI def setupUi(self) -> None: super().setupUi() assert self.ui is not None self.axesList.connect(self.ui.setAxes) self.shapeDetermined.connect(self.ui.setShape)
class RunList(QtWidgets.QTreeWidget): """Shows the list of runs for a given date selection.""" cols = [ 'Run ID', 'Tag', 'Experiment', 'Sample', 'Name', 'Started', 'Completed', 'Records', 'GUID' ] tag_dict = {'': '', 'star': '⭐', 'cross': '❌'} runSelected = Signal(int) runActivated = Signal(int) def __init__(self, parent: Optional[QtWidgets.QWidget] = None): super().__init__(parent) self.setColumnCount(len(self.cols)) self.setHeaderLabels(self.cols) self.itemSelectionChanged.connect(self.selectRun) self.itemActivated.connect(self.activateRun) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showContextMenu) @Slot(QtCore.QPoint) def showContextMenu(self, position: QtCore.QPoint) -> None: model_index = self.indexAt(position) item = self.itemFromIndex(model_index) assert item is not None current_tag_char = item.text(1) menu = QtWidgets.QMenu() copy_icon = self.style().standardIcon( QtWidgets.QStyle.SP_DialogSaveButton) copy_action = menu.addAction(copy_icon, "Copy") window = cast(QCodesDBInspector, self.window()) starAction: QtWidgets.QAction = window.starAction # type: ignore[has-type] starAction.setText( 'Star' if current_tag_char != self.tag_dict['star'] else 'Unstar') menu.addAction(starAction) crossAction: QtWidgets.QAction = window.crossAction # type: ignore[has-type] crossAction.setText('Cross' if current_tag_char != self. tag_dict['cross'] else 'Uncross') menu.addAction(crossAction) action = menu.exec_(self.mapToGlobal(position)) if action == copy_action: QtWidgets.QApplication.clipboard().setText( item.text(model_index.column())) def addRun(self, runId: int, **vals: str) -> None: lst = [str(runId)] tag = vals.get('inspectr_tag', '') lst.append(self.tag_dict.get( tag, tag)) # if the tag is not in tag_dict, display in text lst.append(vals.get('experiment', '')) lst.append(vals.get('sample', '')) lst.append(vals.get('name', '')) lst.append( vals.get('started_date', '') + ' ' + vals.get('started_time', '')) lst.append( vals.get('completed_date', '') + ' ' + vals.get('completed_time', '')) lst.append(str(vals.get('records', ''))) lst.append(vals.get('guid', '')) item = SortableTreeWidgetItem(lst) self.addTopLevelItem(item) def setRuns(self, selection: Dict[int, Dict[str, str]], show_only_star: bool, show_also_cross: bool) -> None: self.clear() # disable sorting before inserting values to avoid performance hit self.setSortingEnabled(False) for runId, record in selection.items(): tag = record.get('inspectr_tag', '') if show_only_star and tag == '': continue elif show_also_cross or tag != 'cross': self.addRun(runId, **record) self.setSortingEnabled(True) for i in range(len(self.cols)): self.resizeColumnToContents(i) def updateRuns(self, selection: Dict[int, Dict[str, str]]) -> None: run_added = False for runId, record in selection.items(): item = self.findItems(str(runId), QtCore.Qt.MatchExactly) if len(item) == 0: self.setSortingEnabled(False) self.addRun(runId, **record) run_added = True elif len(item) == 1: completed = record.get('completed_date', '') + ' ' + record.get( 'completed_time', '') if completed != item[0].text(6): item[0].setText(6, completed) num_records = str(record.get('records', '')) if num_records != item[0].text(7): item[0].setText(7, num_records) else: raise RuntimeError(f"More than one runs found with runId: " f"{runId}") if run_added: self.setSortingEnabled(True) for i in range(len(self.cols)): self.resizeColumnToContents(i) @Slot() def selectRun(self) -> None: selection = self.selectedItems() if len(selection) == 0: return runId = int(selection[0].text(0)) self.runSelected.emit(runId) @Slot(QtWidgets.QTreeWidgetItem, int) def activateRun(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None: runId = int(item.text(0)) self.runActivated.emit(runId)
class MultiDimensionSelector(QtWidgets.QListWidget): """A simple list widget that allows selection of multiple data dimensions.""" #: signal (List[str]) that is emitted when the selection is modified. dimensionSelectionMade = Signal(list) def __init__(self, parent: Optional[QtWidgets.QWidget] = None, dimensionType: str = 'all') -> None: """Constructor. :param parent: parent widget. :param dimensionType: one of ``all``, ``axes``, or ``dependents``. """ super().__init__(parent) self.node: Optional[Node] = None self.dimensionType = dimensionType self.setSelectionMode(self.MultiSelection) self.itemSelectionChanged.connect(self.emitSelection) def setDimensions(self, dimensions: List[str]) -> None: """set the available dimensions. :param dimensions: list of dimension names. """ self.clear() self.addItems(dimensions) def getSelected(self) -> List[str]: """Get selected dimensions. :return: List of dimensions (as strings). """ selectedItems = self.selectedItems() return [s.text() for s in selectedItems] def setSelected(self, selected: List[str]) -> None: """Set dimension selection. :param selected: List of dimensions to be selected. """ for i in range(self.count()): item = self.item(i) if item is not None: if item.text() in selected: item.setSelected(True) else: item.setSelected(False) def emitSelection(self) -> None: self.dimensionSelectionMade.emit(self.getSelected()) def connectNode(self, node: Optional[Node] = None) -> None: """Connect a node. Will result in populating the available options based on dimensions available in the node data. :param node: instance of :class:`.Node` """ if node is None: raise RuntimeError self.node = node if self.dimensionType == 'axes': self.node.dataAxesChanged.connect(self.setDimensions) elif self.dimensionType == 'dependents': self.node.dataDependentsChanged.connect(self.setDimensions) else: self.node.dataFieldsChanged.connect(self.setDimensions)
class DDH5Loader(Node): nodeName = 'DDH5Loader' uiClass = DDH5LoaderWidget useUi = True # nRetries = 5 # retryDelay = 0.01 setProcessOptions = Signal(str, str) def __init__(self, name: str): self._filepath: Optional[str] = None super().__init__(name) self.groupname = 'data' # type: ignore[misc] self.nLoadedRecords = 0 self.loadingThread = QtCore.QThread() self.loadingWorker = _Loader(self.filepath, self.groupname) self.loadingWorker.moveToThread(self.loadingThread) self.loadingThread.started.connect(self.loadingWorker.loadData) self.loadingWorker.dataLoaded.connect(self.onThreadComplete) self.loadingWorker.dataLoaded.connect( lambda x: self.loadingThread.quit()) self.setProcessOptions.connect(self.loadingWorker.setPathAndGroup) @property def filepath(self) -> Optional[str]: return self._filepath @filepath.setter # type: ignore[misc] @updateOption('filepath') def filepath(self, val: str) -> None: self._filepath = val @property def groupname(self) -> str: return self._groupname @groupname.setter # type: ignore[misc] @updateOption('groupname') def groupname(self, val: str) -> None: self._groupname = val # Data processing # def process( self, dataIn: Optional[DataDictBase] = None) -> Optional[Dict[str, Any]]: # TODO: maybe needs an optional way to read only new data from file? -- can make that an option # TODO: implement a threaded version. # this is the flow when process is called due to some trigger if self._filepath is None or self._groupname is None: return None if not os.path.exists(self._filepath): return None if not self.loadingThread.isRunning(): self.loadingWorker.setPathAndGroup(self.filepath, self.groupname) self.loadingThread.start() return None @Slot(object) def onThreadComplete(self, data: Optional[DataDict]) -> None: if data is None: return None title = f"{self.filepath}" data.add_meta('title', title) nrecords = data.nrecords() assert nrecords is not None self.nLoadedRecords = nrecords self.setOutput(dataOut=data) # this makes sure that we analyze the data and emit signals for changes super().process(dataIn=data)
class DimensionCombo(QtWidgets.QComboBox): """A Combo Box that allows selection of a single data dimension. This widget is designed to be used in a node widget. Which type of dimensions are available for selection is set through the ``dimensionType`` option when creating the instance. The widget can be linked to a node using the :meth:`.connectNode` method. After linking, the available options will be populated whenever the data in the node changes. """ #: Signal(str) #: emitted when the user selects a dimension. dimensionSelected = Signal(str) def __init__(self, parent: Optional[QtWidgets.QWidget] = None, dimensionType: str = 'axes') -> None: """Constructor. :param parent: parent widget :param dimensionType: one of `axes`, `dependents` or `all`. """ super().__init__(parent) self.node: Optional[Node] = None self.dimensionType = dimensionType self.clear() self.entries = ['None'] for e in self.entries: self.addItem(e) self.currentTextChanged.connect(self.signalDimensionSelection) def connectNode(self, node: Optional[Node] = None) -> None: """Connect a node. will result in populating the combo box options based on dimensions available in the node data. :param node: instance of :class:`.Node` """ if node is None: raise RuntimeError self.node = node if self.dimensionType == 'axes': self.node.dataAxesChanged.connect(self.setDimensions) elif self.dimensionType == 'dependents': self.node.dataDependentsChanged.connect(self.setDimensions) else: self.node.dataFieldsChanged.connect(self.setDimensions) @updateGuiQuietly def setDimensions(self, dims: Sequence[str]) -> None: """Set the dimensions that are available for selection. :param dims: list of dimensions, as strings. :return: ``None`` """ self.clear() allDims = self.entries + list(dims) for d in allDims: self.addItem(d) @Slot(str) @emitGuiUpdate('dimensionSelected') def signalDimensionSelection(self, val: str) -> str: return val
class FittingNode(Node): uiClass = FittingGui nodeName = "Fitter" default_fitting_options = Signal(object) guess_fitting_options = Signal(object) def __init__(self, name: str): super().__init__(name) self._fitting_options: Optional[FittingOptions] = None def process(self, dataIn: Optional[DataDictBase] = None) -> Optional[Dict[str, Optional[DataDictBase]]]: return self.fitting_process(dataIn) @property def fitting_options(self) -> Optional[FittingOptions]: return self._fitting_options @fitting_options.setter # type: ignore[misc] # https://github.com/python/mypy/issues/1362 @updateOption('fitting_options') def fitting_options(self, opt: Optional[FittingOptions]) -> None: if isinstance(opt, FittingOptions) or opt is None: self._fitting_options = opt else: raise TypeError('Wrong fitting options') def fitting_process(self, dataIn: Optional[DataDictBase] = None) -> Optional[Dict[str, Optional[DataDictBase]]]: if dataIn is None: return None if len(dataIn.axes()) > 1 or len(dataIn.dependents()) > 1: return dict(dataOut=dataIn) dataIn_opt = dataIn.get('__fitting_options__') dataOut = dataIn.copy() # no fitting option selected in gui if self.fitting_options is None: if dataIn_opt is not None: self._fitting_options = dataIn_opt else: return dict(dataOut=dataOut) if dataIn_opt is not None: if DEBUG: print("NODE>>>: ", "Emit initial option from node!", dataIn_opt) self.default_fitting_options.emit(dataIn_opt) # fitting if DEBUG: print("NODE>>>: ", f"node got fitting option {self.fitting_options}") axname = dataIn.axes()[0] x = dataIn.data_vals(axname) y = dataIn.data_vals(dataIn.dependents()[0]) assert isinstance(self.fitting_options, FittingOptions) fit = self.fitting_options.model(x, y) if self.fitting_options.dry_run: guess_params = lmParameters() for pn, pv in fit.guess(x, y).items(): guess_params.add(pn, value=pv) guess_opts = FittingOptions(self.fitting_options.model, guess_params, False) self.guess_fitting_options.emit(guess_opts) if DEBUG: print("NODE>>>: ", f"guess param in node. Emit guess_opts: {guess_opts}") # show dry run result fit_result = fit.run(dry=True) result_y = fit_result.eval(coordinates=x) dataOut['guess'] = dict(values=result_y, axes=[axname, ]) else: fit_result = fit.run(params=self.fitting_options.parameters) assert isinstance(fit_result, FitResult) lm_result = fit_result.lmfit_result if lm_result.success: dataOut['fit'] = dict(values=lm_result.best_fit, axes=[axname,]) dataOut.add_meta('info', lm_result.fit_report()) return dict(dataOut=dataOut) def setupUi(self) -> None: super().setupUi() assert isinstance(self.ui, FittingGui) self.default_fitting_options.connect(self.ui.setDefaultFit) self.guess_fitting_options.connect(self.ui.setGuessParam)