diff --git a/docs/examples/toymodel.py b/docs/examples/toymodel.py index fb28c353b4aa..9b3bc6d865e4 100644 --- a/docs/examples/toymodel.py +++ b/docs/examples/toymodel.py @@ -2,11 +2,12 @@ import math -from qcodes import MockInstrument, MockModel, Parameter, Loop, DataArray +from qcodes import MockInstrument, Parameter, Loop, DataArray +from qcodes.instrument.mock import SingleMockModel from qcodes.utils.validators import Numbers from qcodes.instrument.mock import ArrayGetter -class AModel(MockModel): +class AModel(SingleMockModel): def __init__(self): self._gates = [0.0, 0.0, 0.0] self._excitation = 0.1 diff --git a/qcodes/instrument/mock.py b/qcodes/instrument/mock.py index ec100134e6fd..02c41a1315cc 100644 --- a/qcodes/instrument/mock.py +++ b/qcodes/instrument/mock.py @@ -1,6 +1,7 @@ """Mock instruments for testing purposes.""" import time from datetime import datetime +from uuid import uuid4 from .base import Instrument from .parameter import MultiParameter @@ -283,6 +284,109 @@ def _delattr(self, attr): """ self.ask('method_call', 'delattr', attr) + +# Model is purely in service of mock instruments which *are* tested +# so coverage testing this (by running it locally) would be a waste. +class SingleMockModel: # pragma: no cover + + """ + Base class for models to connect to various MockInstruments. + + Args: + name (str): The server name to create for the model. + Default 'Model-{:.7s}' uses the first 7 characters of + the server's uuid. + + for every instrument that connects to this model, create two methods: + - ``_set(param, value)``: set a parameter on the model + - ``_get(param)``: returns the value of a parameter + ``param`` and the set/return values should all be strings + + If ``param`` and/or ``value`` is not recognized, the method should raise + an error. + + """ + + def __init__(self, name='Model-{:.7s}'): + + self.uuid = uuid4().hex + self.name = name.format(self.uuid) + + def handle_cmd(self, cmd): + """ + Handler for all model queries. + + Args: + cmd (str): Can take several forms: + + - ':?': + calls ``self._get()`` and forwards + the return value. + - '::': + calls ``self._set(, )`` + - ':'. + calls ``self._set(, None)`` + + Returns: + Union(str, None): The parameter value, if ``cmd`` has the form + ':?', otherwise no return. + + Raises: + ValueError: if cmd does not match one of the patterns above. + """ + query = cmd.split(':') + + instrument = query[0] + param = query[1] + + if param[-1] == '?' and len(query) == 2: + return getattr(self, instrument + '_get')(param[:-1]) + + elif len(query) <= 3: + value = query[2] if len(query) == 3 else None + getattr(self, instrument + '_set')(param, value) + + else: + raise ValueError() + + def getattr(self, attr, default=_NoDefault): + """ + Get a (possibly nested) attribute of this model on its server. + + See NestedAttrAccess for details. + """ + return self.ask('method_call', 'getattr', attr, default) + + def setattr(self, attr, value): + """ + Set a (possibly nested) attribute of this model on its server. + + See NestedAttrAccess for details. + """ + self.ask('method_call', 'setattr', attr, value) + + def callattr(self, attr, *args, **kwargs): + """ + Call a (possibly nested) method of this model on its server. + + See NestedAttrAccess for details. + """ + return self.ask('method_call', 'callattr', attr, *args, **kwargs) + + def delattr(self, attr): + """ + Delete a (possibly nested) attribute of this model on its server. + + See NestedAttrAccess for details. + """ + self.ask('method_call', 'delattr', attr) + + def ask(self, func_name, *args, **kwargs): + return self.handle_cmd(args[0]) + + def write(self, func_name, *args, **kwargs): + self.handle_cmd(args[0]) + class ArrayGetter(MultiParameter): """ Example parameter that just returns a single array diff --git a/qcodes/plots/base.py b/qcodes/plots/base.py index 654ed42d8c4d..b326f79b7f0b 100644 --- a/qcodes/plots/base.py +++ b/qcodes/plots/base.py @@ -159,7 +159,8 @@ def get_default_title(self): title_parts.append(location) return ', '.join(title_parts) - def get_label(self, data_array): + @staticmethod + def get_label(data_array): """ Look for a label in data_array falling back on name. @@ -174,7 +175,8 @@ def get_label(self, data_array): return (getattr(data_array, 'label', '') or getattr(data_array, 'name', '')) - def expand_trace(self, args, kwargs): + @staticmethod + def expand_trace(args, kwargs): """ Complete the x, y (and possibly z) data definition for a trace. diff --git a/qcodes/plots/qcmatplotlib.py b/qcodes/plots/qcmatplotlib.py index 135edac3c9e4..0b86ae5ad5f5 100644 --- a/qcodes/plots/qcmatplotlib.py +++ b/qcodes/plots/qcmatplotlib.py @@ -4,8 +4,14 @@ """ from collections import Mapping +from qtpy import QtWidgets + import matplotlib.pyplot as plt from matplotlib.transforms import Bbox +from matplotlib.widgets import Cursor +import mplcursors + + import numpy as np from numpy.ma import masked_invalid, getmask @@ -217,3 +223,138 @@ def save(self, filename=None): default = "{}.png".format(self.get_default_title()) filename = filename or default self.fig.savefig(filename) + + +class ClickWidget: + def __init__(self, dataset): + self._data = {} + BasePlot.expand_trace(args=[dataset], kwargs=self._data) + self._data['xlabel'] = BasePlot.get_label(self._data['x']) + self._data['ylabel'] = BasePlot.get_label(self._data['y']) + self._data['zlabel'] = BasePlot.get_label(self._data['z']) + self._data['xaxis'] = self._data['x'].ndarray[0, :] + self._data['yaxis'] = self._data['y'].ndarray + + self.fig = plt.figure() + + self._lines = [] + self._datacursor = [] + self._cid = 0 + + hbox = QtWidgets.QHBoxLayout() + self.fig.canvas.setLayout(hbox) + hspace = QtWidgets.QSpacerItem(0, + 0, + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + vspace = QtWidgets.QSpacerItem(0, + 0, + QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Expanding) + hbox.addItem(hspace) + + vbox = QtWidgets.QVBoxLayout() + self.crossbtn = QtWidgets.QCheckBox('Cross section') + self.crossbtn.setToolTip("Display extra subplots with selectable cross sections " + "or sums along axis.") + self.sumbtn = QtWidgets.QCheckBox('Sum') + self.sumbtn.setToolTip("Display sums or cross sections.") + + self.crossbtn.toggled.connect(self.toggle_cross) + self.sumbtn.toggled.connect(self.toggle_sum) + self.toggle_cross() + self.toggle_sum() + + vbox.addItem(vspace) + vbox.addWidget(self.crossbtn) + vbox.addWidget(self.sumbtn) + + hbox.addLayout(vbox) + + def toggle_cross(self): + self.remove_plots() + self.fig.clear() + if self._cid: + self.fig.canvas.mpl_disconnect(self._cid) + if self.crossbtn.isChecked(): + self.sumbtn.setEnabled(True) + self.ax = np.empty((2, 2), dtype='O') + self.ax[0, 0] = self.fig.add_subplot(2, 2, 1) + self.ax[0, 1] = self.fig.add_subplot(2, 2, 2) + self.ax[1, 0] = self.fig.add_subplot(2, 2, 3) + self._cid = self.fig.canvas.mpl_connect('button_press_event', self._click) + self._cursor = Cursor(self.ax[0, 0], useblit=True, color='black') + self.toggle_sum() + else: + self.sumbtn.setEnabled(False) + self.ax = np.empty((1, 1), dtype='O') + self.ax[0, 0] = self.fig.add_subplot(1, 1, 1) + self.ax[0, 0].pcolormesh(self._data['x'], + self._data['y'], + self._data['z']) + self.ax[0, 0].set_xlabel(self._data['xlabel']) + self.ax[0, 0].set_ylabel(self._data['ylabel']) + self.fig.tight_layout(rect=(0, 0.07, 0.9, 1)) + self.fig.canvas.draw_idle() + + def toggle_sum(self): + self.remove_plots() + if not self.crossbtn.isChecked(): + return + if self.sumbtn.isChecked(): + self._cursor.set_active(False) + self.ax[1, 0].set_ylim(0, self._data['z'].sum(axis=0).max() * 1.05) + self.ax[0, 1].set_xlim(0, self._data['z'].sum(axis=1).max() * 1.05) + self.ax[1, 0].set_xlabel(self._data['xlabel']) + self.ax[1, 0].set_ylabel("sum of " + self._data['zlabel']) + self.ax[0, 1].set_xlabel("sum of " + self._data['zlabel']) + self.ax[0, 1].set_ylabel(self._data['ylabel']) + self._lines.append(self.ax[0, 1].plot(self._data['z'].sum(axis=1), + self._data['yaxis'], + color='C0', + marker='.')[0]) + self.ax[0, 1].set_title("") + self._lines.append(self.ax[1, 0].plot(self._data['xaxis'], + self._data['z'].sum(axis=0), + color='C0', + marker='.')[0]) + self.ax[1, 0].set_title("") + self._datacursor = mplcursors.cursor(self._lines, multiple=False) + else: + self._cursor.set_active(True) + self.ax[1, 0].set_xlabel(self._data['xlabel']) + self.ax[1, 0].set_ylabel(self._data['zlabel']) + self.ax[0, 1].set_xlabel(self._data['zlabel']) + self.ax[0, 1].set_ylabel(self._data['ylabel']) + self.ax[1, 0].set_ylim(0, self._data['z'].max() * 1.05) + self.ax[0, 1].set_xlim(0, self._data['z'].max() * 1.05) + self.fig.canvas.draw_idle() + + def remove_plots(self): + for line in self._lines: + line.remove() + self._lines = [] + if self._datacursor: + self._datacursor.remove() + + def _click(self, event): + + if event.inaxes == self.ax[0, 0] and not self.sumbtn.isChecked(): + xpos = (abs(self._data['xaxis'] - event.xdata)).argmin() + ypos = (abs(self._data['yaxis'] - event.ydata)).argmin() + self.remove_plots() + + self._lines.append(self.ax[0, 1].plot(self._data['z'][:, xpos], + self._data['yaxis'], + color='C0', + marker='.')[0]) + self.ax[0,1].set_title("{} = {} ".format(self._data['xlabel'], self._data['xaxis'][xpos]), + fontsize='small') + self._lines.append(self.ax[1, 0].plot(self._data['xaxis'], + self._data['z'][ypos, :], + color='C0', + marker='.')[0]) + self.ax[1, 0].set_title("{} = {} ".format(self._data['ylabel'], self._data['yaxis'][ypos]), + fontsize='small') + self._datacursor = mplcursors.cursor(self._lines, multiple=False) + self.fig.canvas.draw() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8094b2e926d8..5efdc93dcf6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ jupyter==1.0.0 numpy==1.11.2 matplotlib==1.5.3 +mplcursors==0.1 pyqtgraph==0.10.0 PyVISA==1.8 PyQt5==5.7.1