From 23a84500a33e85597e504880628339753c7af3f7 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 30 Nov 2015 00:21:27 +0800 Subject: [PATCH] gui/experiments: support scan widgets --- artiq/gui/experiments.py | 93 +++++++++------ artiq/gui/scan.py | 245 +++++++++++++++++++++------------------ 2 files changed, 188 insertions(+), 150 deletions(-) diff --git a/artiq/gui/experiments.py b/artiq/gui/experiments.py index bd0e29ff9..bca980ce3 100644 --- a/artiq/gui/experiments.py +++ b/artiq/gui/experiments.py @@ -16,27 +16,35 @@ logger = logging.getLogger(__name__) class _StringEntry(QtGui.QLineEdit): def __init__(self, argument): QtGui.QLineEdit.__init__(self) - self.setText(argument["value"]) + self.setText(argument["state"]) def update(): - argument["value"] = self.text() + argument["state"] = self.text() self.editingFinished.connect(update) @staticmethod - def default(argdesc): - return "" + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + return procdesc.get("default", "") class _BooleanEntry(QtGui.QCheckBox): def __init__(self, argument): QtGui.QCheckBox.__init__(self) - self.setChecked(argument["value"]) + self.setChecked(argument["state"]) def update(checked): - argument["value"] = checked + argument["state"] = checked self.stateChanged.connect(update) @staticmethod - def default(argdesc): - return False + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + return procdesc.get("default", False) class _EnumerationEntry(QtGui.QComboBox): @@ -44,43 +52,57 @@ class _EnumerationEntry(QtGui.QComboBox): QtGui.QComboBox.__init__(self) choices = argument["desc"]["choices"] self.addItems(choices) - idx = choices.index(argument["value"]) + idx = choices.index(argument["state"]) self.setCurrentIndex(idx) def update(index): - argument["value"] = choices[index] + argument["state"] = choices[index] self.currentIndexChanged.connect(update) @staticmethod - def default(argdesc): - return argdesc["choices"][0] + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + if "default" in procdesc: + return procdesc["default"] + else: + return procdesc["choices"][0] class _NumberEntry(QtGui.QDoubleSpinBox): def __init__(self, argument): QtGui.QDoubleSpinBox.__init__(self) - argdesc = argument["desc"] - scale = argdesc["scale"] - self.setDecimals(argdesc["ndecimals"]) - self.setSingleStep(argdesc["step"]/scale) - if argdesc["min"] is not None: - self.setMinimum(argdesc["min"]/scale) + procdesc = argument["desc"] + scale = procdesc["scale"] + self.setDecimals(procdesc["ndecimals"]) + self.setSingleStep(procdesc["step"]/scale) + if procdesc["min"] is not None: + self.setMinimum(procdesc["min"]/scale) else: self.setMinimum(float("-inf")) - if argdesc["max"] is not None: - self.setMaximum(argdesc["max"]/scale) + if procdesc["max"] is not None: + self.setMaximum(procdesc["max"]/scale) else: self.setMaximum(float("inf")) - if argdesc["unit"]: - self.setSuffix(" " + argdesc["unit"]) + if procdesc["unit"]: + self.setSuffix(" " + procdesc["unit"]) - self.setValue(argument["value"]/scale) + self.setValue(argument["state"]/scale) def update(value): - argument["value"] = value*scale + argument["state"] = value*scale self.valueChanged.connect(update) - + @staticmethod - def default(argdesc): - return 0.0 + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + if "default" in procdesc: + return procdesc["default"] + else: + return 0.0 _argty_to_entry = { @@ -254,16 +276,11 @@ class ExperimentManager: arguments = OrderedDict() arginfo = self.explist[expname]["arguments"] for name, (procdesc, group) in arginfo: - argdesc = dict(procdesc) - if "default" in argdesc: - value = argdesc["default"] - del argdesc["default"] - else: - value = _argty_to_entry[argdesc["ty"]].default(argdesc) + state = _argty_to_entry[procdesc["ty"]].default_state(procdesc) arguments[name] = { - "desc": argdesc, + "desc": procdesc, "group": group, - "value": value # mutated by entries + "state": state # mutated by entries } self.submission_arguments[expname] = arguments return arguments @@ -289,7 +306,11 @@ class ExperimentManager: scheduling = self.get_submission_scheduling(expname) options = self.get_submission_options(expname) arguments = self.get_submission_arguments(expname) - argument_values = {k: v["value"] for k, v in arguments.items()} + + argument_values = dict() + for name, argument in arguments.items(): + entry_cls = _argty_to_entry[argument["desc"]["ty"]] + argument_values[name] = entry_cls.state_to_value(argument["state"]) expid = { "log_level": options["log_level"], diff --git a/artiq/gui/scan.py b/artiq/gui/scan.py index c98d47509..8ef1bbffc 100644 --- a/artiq/gui/scan.py +++ b/artiq/gui/scan.py @@ -1,26 +1,60 @@ +import logging +from collections import OrderedDict + from quamash import QtGui from pyqtgraph import LayoutWidget -class _Range(LayoutWidget): - def __init__(self, global_min, global_max, global_step, unit, scale, ndecimals): +logger = logging.getLogger(__name__) + + +class _NoScan(LayoutWidget): + def __init__(self, procdesc, state): LayoutWidget.__init__(self) - self.scale = scale + scale = procdesc["scale"] + self.value = QtGui.QDoubleSpinBox() + self.value.setDecimals(procdesc["ndecimals"]) + if procdesc["global_min"] is not None: + self.value.setMinimum(procdesc["global_min"]/scale) + else: + self.value.setMinimum(float("-inf")) + if procdesc["global_max"] is not None: + self.value.setMaximum(procdesc["global_max"]/scale) + else: + self.value.setMaximum(float("inf")) + self.value.setSingleStep(procdesc["global_step"]/scale) + if procdesc["unit"]: + self.value.setSuffix(" " + procdesc["unit"]) + self.addWidget(QtGui.QLabel("Value:"), 0, 0) + self.addWidget(self.value, 0, 1) + + self.value.setValue(state["value"]/scale) + def update(value): + state["value"] = value*scale + self.value.valueChanged.connect(update) + + +# TODO: prevent max < min +class _Range(LayoutWidget): + def __init__(self, procdesc, state): + LayoutWidget.__init__(self) + + scale = procdesc["scale"] def apply_properties(spinbox): - spinbox.setDecimals(ndecimals) - if global_min is not None: - spinbox.setMinimum(global_min/self.scale) + spinbox.setDecimals(procdesc["ndecimals"]) + if procdesc["global_min"] is not None: + spinbox.setMinimum(procdesc["global_min"]/scale) else: spinbox.setMinimum(float("-inf")) - if global_max is not None: - spinbox.setMaximum(global_max/self.scale) + if procdesc["global_max"] is not None: + spinbox.setMaximum(procdesc["global_max"]/scale) else: spinbox.setMaximum(float("inf")) - if global_step is not None: - spinbox.setSingleStep(global_step/self.scale) - if unit: - spinbox.setSuffix(" " + unit) + if procdesc["global_step"] is not None: + spinbox.setSingleStep(procdesc["global_step"]/scale) + if procdesc["unit"]: + spinbox.setSuffix(" " + procdesc["unit"]) self.addWidget(QtGui.QLabel("Min:"), 0, 0) self.min = QtGui.QDoubleSpinBox() @@ -38,121 +72,104 @@ class _Range(LayoutWidget): self.npoints.setValue(10) self.addWidget(self.npoints, 2, 1) - def set_values(self, min, max, npoints): - self.min.setValue(min/self.scale) - self.max.setValue(max/self.scale) - self.npoints.setValue(npoints) + self.min.setValue(state["min"]/scale) + self.max.setValue(state["max"]/scale) + self.npoints.setValue(state["npoints"]) + def update_min(value): + state["min"] = value*scale + def update_max(value): + state["min"] = value*scale + def update_npoints(value): + state["npoints"] = value + self.min.valueChanged.connect(update_min) + self.max.valueChanged.connect(update_max) + self.npoints.valueChanged.connect(update_npoints) - def get_values(self): - min = self.min.value() - max = self.max.value() - if min > max: - raise ValueError("Minimum scan boundary must be less than maximum") - return { - "min": min*self.scale, - "max": max*self.scale, - "npoints": self.npoints.value() - } + +# TODO: use QRegExpValidator to prevent invalid input +class _Explicit(LayoutWidget): + def __init__(self, state): + LayoutWidget.__init__(self) + + self.value = QtGui.QLineEdit() + self.addWidget(QtGui.QLabel("Sequence:"), 0, 0) + self.addWidget(self.value, 0, 1) + + self.value.setText(" ".join([str(x) for x in state["sequence"]])) + def update(): + state["sequence"] = [float(x) for x in self.value.text().split()] + self.value.editingFinished.connect(update) class ScanController(LayoutWidget): def __init__(self, argument): LayoutWidget.__init__(self) + self.argument = argument self.stack = QtGui.QStackedWidget() self.addWidget(self.stack, 1, 0, colspan=4) - argdesc = argument["desc"] - self.scale = argdesc["scale"] + procdesc = argument["desc"] + state = argument["state"] + self.widgets = OrderedDict() + self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"]) + self.widgets["LinearScan"] = _Range(procdesc, state["LinearScan"]) + self.widgets["RandomScan"] = _Range(procdesc, state["RandomScan"]) + self.widgets["ExplicitScan"] = _Explicit(state["ExplicitScan"]) + for widget in self.widgets.values(): + self.stack.addWidget(widget) - gmin, gmax = argdesc["global_min"], argdesc["global_max"] - gstep = argdesc["global_step"] - unit = argdesc["unit"] - ndecimals = argdesc["ndecimals"] - - self.v_noscan = QtGui.QDoubleSpinBox() - self.v_noscan.setDecimals(ndecimals) - if gmin is not None: - self.v_noscan.setMinimum(gmin/self.scale) - else: - self.v_noscan.setMinimum(float("-inf")) - if gmax is not None: - self.v_noscan.setMaximum(gmax/self.scale) - else: - self.v_noscan.setMaximum(float("inf")) - self.v_noscan.setSingleStep(gstep/self.scale) - if unit: - self.v_noscan.setSuffix(" " + unit) - self.v_noscan_gr = LayoutWidget() - self.v_noscan_gr.addWidget(QtGui.QLabel("Value:"), 0, 0) - self.v_noscan_gr.addWidget(self.v_noscan, 0, 1) - self.stack.addWidget(self.v_noscan_gr) - - self.v_linear = _Range(gmin, gmax, gstep, unit, self.scale, ndecimals) - self.stack.addWidget(self.v_linear) - - self.v_random = _Range(gmin, gmax, gstep, unit, self.scale, ndecimals) - self.stack.addWidget(self.v_random) - - self.v_explicit = QtGui.QLineEdit() - self.v_explicit_gr = LayoutWidget() - self.v_explicit_gr.addWidget(QtGui.QLabel("Sequence:"), 0, 0) - self.v_explicit_gr.addWidget(self.v_explicit, 0, 1) - self.stack.addWidget(self.v_explicit_gr) - - self.noscan = QtGui.QRadioButton("No scan") - self.linear = QtGui.QRadioButton("Linear") - self.random = QtGui.QRadioButton("Random") - self.explicit = QtGui.QRadioButton("Explicit") - radiobuttons = QtGui.QButtonGroup() - for n, b in enumerate([self.noscan, self.linear, - self.random, self.explicit]): + self.radiobuttons = OrderedDict() + self.radiobuttons["NoScan"] = QtGui.QRadioButton("No scan") + self.radiobuttons["LinearScan"] = QtGui.QRadioButton("Linear") + self.radiobuttons["RandomScan"] = QtGui.QRadioButton("Random") + self.radiobuttons["ExplicitScan"] = QtGui.QRadioButton("Explicit") + scan_type = QtGui.QButtonGroup() + for n, b in enumerate(self.radiobuttons.values()): self.addWidget(b, 0, n) - radiobuttons.addButton(b) - b.toggled.connect(self.select_page) + scan_type.addButton(b) + b.toggled.connect(self._scan_type_toggled) + + selected = argument["state"]["selected"] + self.radiobuttons[selected].setChecked(True) @staticmethod - def default(argdesc): - return {"ty": "NoScan", "value": 0.0} + def state_to_value(state): + selected = state["selected"] + r = dict(state[selected]) + r["ty"] = selected + return r - def select_page(self): - if self.noscan.isChecked(): - self.stack.setCurrentWidget(self.v_noscan_gr) - elif self.linear.isChecked(): - self.stack.setCurrentWidget(self.v_linear) - elif self.random.isChecked(): - self.stack.setCurrentWidget(self.v_random) - elif self.explicit.isChecked(): - self.stack.setCurrentWidget(self.v_explicit_gr) + @staticmethod + def default_state(procdesc): + scale = procdesc["scale"] + state = { + "selected": "NoScan", + "NoScan": {"value": 0.0}, + "LinearScan": {"min": 0.0, "max": 100.0*scale, "npoints": 10}, + "RandomScan": {"min": 0.0, "max": 100.0*scale, "npoints": 10}, + "ExplicitScan": {"sequence": []} + } + if "default" in procdesc: + default = procdesc["default"] + ty = default["ty"] + state["selected"] = ty + if ty == "NoScan": + state["NoScan"]["value"] = default["value"] + elif ty == "LinearScan" or ty == "RandomScan": + for d in state["LinearScan"], state["RandomScan"]: + d["min"] = default["min"] + d["max"] = default["max"] + d["npoints"] = default["npoints"] + elif ty == "ExplicitScan": + state["ExplicitScan"]["sequence"] = default["sequence"] + else: + logger.warning("unknown default type: %s", ty) + return state - def get_argument_value(self): - if self.noscan.isChecked(): - return {"ty": "NoScan", "value": self.v_noscan.value()*self.scale} - elif self.linear.isChecked(): - d = {"ty": "LinearScan"} - d.update(self.v_linear.get_values()) - return d - elif self.random.isChecked(): - d = {"ty": "RandomScan"} - d.update(self.v_random.get_values()) - return d - elif self.explicit.isChecked(): - sequence = [float(x) for x in self.v_explicit.text().split()] - return {"ty": "ExplicitScan", "sequence": sequence} - - def set_argument_value(self, d): - if d["ty"] == "NoScan": - self.noscan.setChecked(True) - self.v_noscan.setValue(d["value"]/self.scale) - elif d["ty"] == "LinearScan": - self.linear.setChecked(True) - self.v_linear.set_values(d["min"], d["max"], d["npoints"]) - elif d["ty"] == "RandomScan": - self.random.setChecked(True) - self.v_random.set_values(d["min"], d["max"], d["npoints"]) - elif d["ty"] == "ExplicitScan": - self.explicit.setChecked(True) - self.v_explicit.insert(" ".join( - [str(x) for x in d["sequence"]])) - else: - raise ValueError("Unknown scan type '{}'".format(d["ty"])) + def _scan_type_toggled(self): + for ty, button in self.radiobuttons.items(): + if button.isChecked(): + self.stack.setCurrentWidget(self.widgets[ty]) + self.argument["state"]["selected"] = ty + break