Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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())
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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)))
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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()
Exemplo n.º 9
0
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()
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
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)
Exemplo n.º 17
0
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',
        ])
Exemplo n.º 18
0
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()
Exemplo n.º 19
0
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)
Exemplo n.º 20
0
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)
Exemplo n.º 21
0
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)
Exemplo n.º 22
0
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)
Exemplo n.º 23
0
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)
Exemplo n.º 24
0
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
Exemplo n.º 25
0
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)