Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crosssection viewer #480

Closed
wants to merge 10 commits into from
5 changes: 3 additions & 2 deletions docs/examples/toymodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions qcodes/instrument/mock.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
- ``<instrument>_set(param, value)``: set a parameter on the model
- ``<instrument>_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:

- '<instrument>:<parameter>?':
calls ``self.<instrument>_get(<parameter>)`` and forwards
the return value.
- '<instrument>:<parameter>:<value>':
calls ``self.<instrument>_set(<parameter>, <value>)``
- '<instrument>:<parameter>'.
calls ``self.<instrument>_set(<parameter>, None)``

Returns:
Union(str, None): The parameter value, if ``cmd`` has the form
'<instrument>:<parameter>?', 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
Expand Down
6 changes: 4 additions & 2 deletions qcodes/plots/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down
141 changes: 141 additions & 0 deletions qcodes/plots/qcmatplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down