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

Live plotting #10

Merged
merged 24 commits into from
Jan 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5f29f28
back-reference from DataArrays to DataSets
alexcjohnson Jan 13, 2016
95875af
live plotting
alexcjohnson Jan 13, 2016
b6ad830
infer setpoint data and dimensionality in Plot calls
alexcjohnson Jan 13, 2016
d033e68
ion/ioff doesn't seem to matter
alexcjohnson Jan 13, 2016
69714ca
update instead of redraw plot
alexcjohnson Jan 14, 2016
2d242d2
Plot.draw() is no longer used
alexcjohnson Jan 14, 2016
e5328d2
plot halt shortcut
alexcjohnson Jan 14, 2016
edb72d0
plot docstrings
alexcjohnson Jan 14, 2016
3408bc7
colorbar
alexcjohnson Jan 14, 2016
a22dfca
automatic labels on plots
alexcjohnson Jan 15, 2016
57cd01a
wire up figsize correctly
alexcjohnson Jan 15, 2016
cbb6edc
move Plot -> MatPlot so we can have different plotting backends
alexcjohnson Jan 15, 2016
1ec2f69
comment update
alexcjohnson Jan 15, 2016
6cc56f0
break out BasePlot from MatPlot
alexcjohnson Jan 18, 2016
a3dfed6
take out initial update call in HiddenUpdateWidget, so we plot init i…
alexcjohnson Jan 18, 2016
10f5625
not using MatPlot.set_title anymore
alexcjohnson Jan 18, 2016
537db90
document BasePlot.traces requirement
alexcjohnson Jan 18, 2016
20455ea
DataArray.__len__
alexcjohnson Jan 20, 2016
c4b5b2c
reset update timer when traces are added to a plot
alexcjohnson Jan 20, 2016
1374b87
fix MatPlot docstring
alexcjohnson Jan 20, 2016
a1ab3c1
some color constants copied in from plotly
alexcjohnson Jan 20, 2016
66a8526
pyqtgraph
alexcjohnson Jan 20, 2016
58ca8ad
comment update
alexcjohnson Jan 20, 2016
8d303dd
pyqtgraph label colorscale, and tweak styling
alexcjohnson Jan 20, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
483 changes: 161 additions & 322 deletions Qcodes example.ipynb

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions qcodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
from qcodes.station import Station
from qcodes.loops import get_bg, halt_bg, Loop, Task, Wait

# hack for code that should only be imported into the main (notebook) thread
# see: http://stackoverflow.com/questions/15411967
# in particular, importing matplotlib in the side processes takes a long
# time and spins up other processes in order to try and get a front end
import sys as _sys
if 'ipy' in repr(_sys.stdout):
from qcodes.plots.matplotlib import MatPlot
from qcodes.plots.pyqtgraph import QtPlot

from qcodes.data.manager import get_data_manager
from qcodes.data.data_set import DataMode, DataSet
from qcodes.data.data_array import DataArray
Expand Down
25 changes: 23 additions & 2 deletions qcodes/data/data_array.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import numpy as np
import collections

import qcodes


class DataArray(object):
'''
Expand Down Expand Up @@ -37,6 +35,10 @@ def __init__(self, parameter=None, name=None, label=None, array_id=None,
self.size = size
self._preset = False

# store a reference up to the containing DataSet
# this also lets us make sure a DataArray is only in one DataSet
self._data_set = None

self.data = None
if preset_data is not None:
self.init_data(preset_data)
Expand All @@ -47,6 +49,18 @@ def __init__(self, parameter=None, name=None, label=None, array_id=None,
self.last_saved_index = None
self.modified_range = None

@property
def data_set(self):
return self._data_set

@data_set.setter
def data_set(self, new_data_set):
if (self._data_set is not None and
new_data_set is not None and
self._data_set != new_data_set):
raise RuntimeError('A DataArray can only be part of one DataSet')
self._data_set = new_data_set

def nest(self, size, action_index=None, set_array=None):
'''
nest this array inside a new outer loop
Expand Down Expand Up @@ -162,6 +176,13 @@ def __getattr__(self, key):

return getattr(self.data, key)

def __len__(self):
'''
must be explicitly delegated to, because len() will look for this
attribute to already exist
'''
return len(self.data)

def _flat_index(self, indices, index_fill):
indices = indices + index_fill[len(indices):]
return np.ravel_multi_index(tuple(zip(indices)), self.size)[0]
Expand Down
22 changes: 20 additions & 2 deletions qcodes/data/data_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def sync(self):
'''
synchronize this data set with a possibly newer version either
in storage or on the DataServer, depending on its mode

returns: boolean, is this DataSet live on the server
'''
# TODO: sync implies bidirectional... and it could be!
# we should keep track of last sync timestamp and last modification
Expand All @@ -179,7 +181,10 @@ def sync(self):
# LOCAL DataSet - just read it in
# TODO: compare timestamps to know if we need to read?
self.read()
return
return False
# TODO - for remote live plotting, maybe set some timestamp
# threshold and call it static after it's been dormant a long time?
# I'm thinking like a minute, or ten? Maybe it's configurable?

with self.data_manager.query_lock:
if self.is_on_server:
Expand All @@ -198,22 +203,35 @@ def sync(self):
# but the DataSet is still on the server,
# so we got the data, and don't need to read.
self.mode = DataMode.LOCAL
return
return False
return True
else:
# this DataSet *thought* it was on the server, but it wasn't,
# so we haven't synced yet and need to read from storage
self.mode = DataMode.LOCAL
self.read()
return False

def add_array(self, data_array):
'''
add one DataArray to this DataSet

note: DO NOT just set data_set.arrays[id] = data_array
because this will not check for overriding, nor set the
reference back to this DataSet. It would also allow you to
load the array in with different id than it holds itself.

'''
# TODO: mask self.arrays so you *can't* set it directly

if data_array.array_id in self.arrays:
raise ValueError('array_id {} already exists in this '
'DataSet'.format(data_array.array_id))
self.arrays[data_array.array_id] = data_array

# back-reference to the DataSet
data_array.data_set = self

def _clean_array_ids(self, arrays):
'''
replace action_indices tuple with compact string array_ids
Expand Down
4 changes: 2 additions & 2 deletions qcodes/data/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def read_one_file(self, data_set, f, ids_read):
set_array = DataArray(label=labels[i], array_id=array_id,
set_arrays=set_arrays, size=set_size)
set_array.init_data()
arrays[array_id] = set_array
data_set.add_array(set_array)

set_arrays = set_arrays + (set_array, )
ids_read.add(array_id)
Expand All @@ -300,7 +300,7 @@ def read_one_file(self, data_set, f, ids_read):
data_array = DataArray(label=labels[i], array_id=array_id,
set_arrays=set_arrays, size=size)
data_array.init_data()
arrays[array_id] = data_array
data_set.add_array(data_array)
data_arrays.append(data_array)
ids_read.add(array_id)

Expand Down
180 changes: 180 additions & 0 deletions qcodes/plots/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
'''
Live plotting in Jupyter notebooks
'''
from IPython.display import display

from qcodes.widgets.widgets import HiddenUpdateWidget


class BasePlot(object):
'''
create an auto-updating plot connected to a Jupyter notebook

interval: period in seconds between update checks
default 1

data_keys: which keys in trace config can contain data
that we should look for updates in.
default 'xyz' (treated as a sequence) but add more if
for example marker size or color can contain data
'''
def __init__(self, interval=1, data_keys='xyz'):
self.data_keys = data_keys
self.traces = []
self.data_updaters = set()

self.interval = interval
self.update_widget = HiddenUpdateWidget(self.update, interval)
display(self.update_widget)

def add(self, *args, updater=None, **kwargs):
'''
add one trace to this plot

args: optional way to provide x/y/z data without keywords
If the last one is 1D, may be `y` or `x`, `y`
If the last one is 2D, may be `z` or `x`, `y`, `z`

updater: a callable (with no args) that updates the data in this trace
if omitted, we will look for DataSets referenced in this data, and
call their sync methods.

kwargs: after inserting info found in args and possibly in set_arrays
into `x`, `y`, and optionally `z`, these are passed along to
self.add_to_plot
'''
self.expand_trace(args, kwargs)
self.add_to_plot(**kwargs)
self.add_updater(updater, kwargs)

def add_to_plot(self, **kwargs):
'''
add a trace the plot itself (typically called by self.add,
which incorporates args into kwargs, so the subclass doesn't
need to worry about this). Data will be in `x`, `y`, and optionally
`z`

should be implemented by a subclass, and each call should append
a dictionary to self.traces, containing at least {'config': kwargs}
'''
raise NotImplementedError

def add_updater(self, updater, plot_config):
if updater is not None:
self.data_updaters.add(updater)
else:
for key in self.data_keys:
data_array = plot_config.get(key, '')
if hasattr(data_array, 'data_set'):
self.data_updaters.add(data_array.data_set.sync)

if self.data_updaters:
self.update_widget.interval = self.interval

def get_default_title(self):
'''
the default title for a plot is just a list of DataSet locations
'''
title_parts = []
for trace in self.traces:
config = trace['config']
for part in self.data_keys:
data_array = config.get(part, '')
if hasattr(data_array, 'data_set'):
location = data_array.data_set.location
if location not in title_parts:
title_parts.append(location)
return ', '.join(title_parts)

def get_label(self, data_array):
'''
look for a label, falling back on name.
'''
return (getattr(data_array, 'label', '') or
getattr(data_array, 'name', ''))

def expand_trace(self, args, kwargs):
'''
the x, y (and potentially z) data for a trace may be provided
as positional rather than keyword args. The allowed forms are
[y] or [x, y] if the last arg is 1D, and
[z] or [x, y, z] if the last arg is 2D

also, look in the main data array (`z` if it exists, else `y`)
for set_arrays that can provide the `x` (and potentially `y`) data

even if we allow data in other attributes (like marker size/color)
by providing a different self.data_keys, set_arrays will only
contribute x from y, or x & y from z, so we don't use data_keys here
'''
if args:
if hasattr(args[-1][0], '__len__'):
# 2D (or higher... but ignore this for now)
# this test works for both numpy arrays and bare sequences
axletters = 'xyz'
ndim = 2
else:
axletters = 'xy'
ndim = 1

if len(args) not in (1, len(axletters)):
raise ValueError('{}D data needs 1 or {} unnamed args'.format(
ndim, len(axletters)))

arg_axletters = axletters[-len(args):]

for arg, arg_axletters in zip(args, arg_axletters):
if arg_axletters in kwargs:
raise ValueError(arg_axletters + ' data provided twice')
kwargs[arg_axletters] = arg

# reset axletters, we may or may not have found them above
axletters = 'xyz' if 'z' in kwargs else 'xy'
main_data = kwargs[axletters[-1]]
if hasattr(main_data, 'set_arrays'):
num_axes = len(axletters) - 1
# things will probably fail if we try to plot arrays of the
# wrong dimension... but we'll give it a shot anyway.
set_arrays = main_data.set_arrays[-num_axes:]
# for 2D: y is outer loop, which is earlier in set_arrays,
# and x is the inner loop... is this the right convention?
set_axletters = reversed(axletters[:-1])
for axletter, set_array in zip(set_axletters, set_arrays):
if axletter not in kwargs:
kwargs[axletter] = set_array

def update(self):
'''
update the data in this plot, using the updaters given with
MatPlot.add() or in the included DataSets, then include this in
the plot

this is a wrapper routine that the update widget calls,
inside this we call self.update() which should be subclassed
'''
any_updates = False
for updater in self.data_updaters:
updates = updater()
if updates is not False:
any_updates = True

self.update_plot()

# once all updaters report they're finished (by returning exactly
# False) we stop updating the plot.
if any_updates is False:
self.halt()

def update_plot(self):
'''
update the plot itself (typically called by self.update).
should be implemented by a subclass
'''
raise NotImplementedError

def halt(self):
'''
stop automatic updates to this plot, by canceling its update widget
'''
if hasattr(self, 'update_widget'):
self.update_widget.halt()
Loading