Skip to content

Commit 1d0ea1a

Browse files
authored
Merge pull request #51 from dihm/runviewer-docs
Initial pass at Runviewer docs
2 parents 72e5f26 + d412be3 commit 1d0ea1a

File tree

11 files changed

+253
-2
lines changed

11 files changed

+253
-2
lines changed

docs/source/api/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
API Reference
2+
=============
3+
4+
.. automodule:: runviewer.__main__
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

docs/source/conf.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# add these directories to sys.path here. If the directory is relative to the
1111
# documentation root, use os.path.abspath to make it absolute, like shown here.
1212
#
13+
import copy
1314
import os
1415
from pathlib import Path
1516
from m2r import MdInclude
@@ -49,6 +50,25 @@
4950
]
5051

5152
autodoc_typehints = 'description'
53+
numfig = True
54+
autodoc_mock_imports = ['labscript_utils']
55+
56+
# mock missing site packages methods
57+
import site
58+
mock_site_methods = {
59+
# Format:
60+
# method name: return value
61+
'getusersitepackages': '',
62+
'getsitepackages': []
63+
}
64+
__fn = None
65+
for __name, __rval in mock_site_methods.items():
66+
if not hasattr(site, __name):
67+
__fn = lambda *args, __rval=copy.deepcopy(__rval), **kwargs: __rval
68+
setattr(site, __name, __fn)
69+
del __name
70+
del __rval
71+
del __fn
5272

5373
# Prefix each autosectionlabel with the name of the document it is in and a colon
5474
autosectionlabel_prefix_document = True
@@ -223,3 +243,43 @@ def setup(app):
223243
img_path=img_path
224244
)
225245
)
246+
247+
# hooks to test docstring coverage
248+
app.connect('autodoc-process-docstring', doc_coverage)
249+
app.connect('build-finished', doc_report)
250+
251+
252+
members_to_watch = ['module', 'class', 'function', 'exception', 'method', 'attribute']
253+
doc_count = 0
254+
undoc_count = 0
255+
undoc_objects = []
256+
undoc_print_objects = False
257+
258+
259+
def doc_coverage(app, what, name, obj, options, lines):
260+
global doc_count
261+
global undoc_count
262+
global undoc_objects
263+
264+
if (what in members_to_watch and len(lines) == 0):
265+
# blank docstring detected
266+
undoc_count += 1
267+
undoc_objects.append(name)
268+
else:
269+
doc_count += 1
270+
271+
272+
def doc_report(app, exception):
273+
global doc_count
274+
global undoc_count
275+
global undoc_objects
276+
# print out report of documentation coverage
277+
total_docs = undoc_count + doc_count
278+
if total_docs != 0:
279+
print(f'\nAPI Doc coverage of {doc_count/total_docs:.1%}')
280+
if undoc_print_objects or os.environ.get('READTHEDOCS'):
281+
print('\nItems lacking documentation')
282+
print('===========================')
283+
print(*undoc_objects, sep='\n')
284+
else:
285+
print('No docs counted, run \'make clean\' then rebuild to get the count.')
131 KB
Loading
76.6 KB
Loading
139 KB
Loading

docs/source/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
runviewer
77
=========
88

9+
Visualizes hardware-timed experiment instructions.
10+
911
.. toctree::
1012
:maxdepth: 2
1113
:hidden:
1214
:caption: DOCUMENTATION
1315

16+
introduction
17+
usage
18+
api/index
19+
1420

1521

1622
.. toctree::

docs/source/introduction.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Introduction
2+
============
3+
4+
Runviewer is used for viewing, graphically, the expected changes in each output across one
5+
or more shots, and is shown in :numref:`fig-interface`. Its use is optional, but can be extremely useful for
6+
debugging the behaviour of experiment logic. The output traces are generated directly from
7+
the set of hardware instructions stored in a given hdf5 file. This provides a faithful representation
8+
of what the hardware will actually do. In effect, runviewer provides a low level
9+
representation of the experiment, which complements the high level representation provided
10+
by the experiment logic written using the labscript API. As such, runviewer traces provide
11+
a way to view the quantisation of outputs, [2]_ which can be seen in the `central_Bq` and
12+
`central_bias_z_coil` channels in :numref:`fig-interface`. You can also view the pseudoclock outputs.
13+
The `pulseblaster_0_ni_clock` and `pulseblaster_0_novatech_clock` channels demonstrate
14+
the independent clocking of devices from a single PulseBlaster pseudoclock. Similarly,
15+
`pulseblaster_1_clock` shows an entirely independent secondary pseudoclock.
16+
17+
.. _fig-interface:
18+
19+
.. figure:: img/runviewer_interface.png
20+
:alt: Runviewer runviewer
21+
22+
An example of the runviewer interface.
23+
24+
.. rubric:: Footnotes
25+
26+
.. [1] Documentation taken from Starkey, Phillip T. *A software framework for control and automation of precisely timed experiments*
27+
PhD Thesis, Monash University (2019) https://doi.org/10.26180/5d1db8ffe29ef
28+
29+
.. [2] While this is always true in time, the output values may not be correctly quantised if the labscript
30+
device implementation does not quantise the output values correctly and instead relies on BLACS,
31+
the device programming API or the device firmware, to correctly quantise the output values.
62.6 KB
Binary file not shown.

docs/source/usage.rst

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
Usage
2+
=====
3+
4+
While the textual based interface of labscript is ideal for defining experiment logic in ‘high
5+
level’ terms, there can often be a discrepancy between what was intended and what was
6+
actually commanded. This is particularly prevalent in situations where more complex control
7+
flow features (such as while loops or parameterised function) are used as this increases
8+
the abstraction between the language used to command the output and the actions of the
9+
output. Runviewer exists to bridge this gap, allowing us to maintain the benefits of textual
10+
control of the experiment without losing the benefits of the graphical representation of the
11+
experiment logic. Runviewer achieves this by producing a series of plots containing the
12+
output state for each channel. These plots are ‘reverse-engineered’ from the hardware instructions
13+
stored in the hdf5 file, and are thus a faithful representation of what each output
14+
channel should do during an experiment (provided of course that the reverse engineering
15+
code is accurate).
16+
There are thus several uses for runviewer. The most important is the ability to graphically
17+
observe the experiment logic. This allows a user to easily observe experiment features
18+
such as the shape of complex ramps or the synchronisation between events on different
19+
channels. Runviewer also supports simultaneous display of traces from multiple shots providing,
20+
for example, a means to see how an output trace changes when a global variable is
21+
adjusted. Finally, comparisons between expected output in runviewer, and observed output
22+
on an oscilloscope can make debugging hardware problems quicker.
23+
24+
Generating output traces
25+
------------------------
26+
27+
Runviewer generates the displayed output traces by processing the hardware instructions
28+
stored in the hdf5 shot file. We specifically reconstruct the output from the lowest level
29+
description of the experiment logic in order to accurately represent what the output hardware
30+
will do during an experiment. In order to support a diverse range of hardware, part
31+
of the reconstruction process is handled by device specific code that must be written by a
32+
developer when adding support for a new device. This device specific code simulates how
33+
the device processes hardware instructions and updates output states. It is discussed in
34+
more detail in :doc:`labscript-devices <labscript-devices:index>`, so for the purposes of this
35+
section we’ll only cover generally what such
36+
code should do. The reconstruction algorithm is then as follows:
37+
38+
#. The master pseudoclock device is identified from the HDF5 file.
39+
#. We import the device specific runviewer class for the master pseudoclock and request that it generate the traces for its outputs. As this is the master pseudoclock, we instruct the device specific code that there is not anything controlling the timing of this device (the need to do this will become apparent in a later step).
40+
#. | The device specific code generates a set of output traces (as it sees fit) and returns these traces to runviewer by calling a provided runviewer method, indicating that these traces should be available for display. This allows the device to produce as many traces as it likes, without limitation by the runviewer architecture. This is critical, as it removes the need for runviewer to support specific output types. Instead, this support is baked-in to the device specific code, which should already be aware of the output capabilities of the device.
41+
|
42+
| If timing information was provided by runviewer (which is the case for all devices except the master pseudoclock, see step 5 below), then it is used by the device code to generate the correct timing of the output traces. For example, a Novatech DDS9m only stores a table of output state changes, so the timing information of the parent ClockLine is needed. Similarly, the timing of a secondary pseudoclock is dependent on state changes to the parent Trigger line.
43+
#. The device specific code then returns, to runviewer, a dictionary of traces for any ClockLines or Triggers assigned to digital outputs of the device (which may or may not have already been provided to runviewer for display in the previous step).
44+
#. Runviewer iterates over this dictionary, and finds all devices that are children of each ClockLine or Trigger. For each device, the device specific code is imported and called as in step 2, except that this time we provide the device specific code with the trace for the ClockLine or Trigger so that it can generate output traces with the correct timing. The device specific code then follows step 3 and runviewer repeats steps 3 to 5 recursively until all devices have been processed.
45+
46+
The graphical interface
47+
-----------------------
48+
49+
The graphical interface of runviewer comprises 3 sections (see :numref:`fig-overview`). The first section
50+
manages the loading of shots into runviewer. Here you can enable (or disable) shots for
51+
plotting, choose the plot colour, and choose whether markers for shutter open and close
52+
times should be displayed. The second section manages the channels that are to be plotted.
53+
These channels can be reordered using the controls to the left, which then affects the order
54+
in which the plots appear. The list displays the union of all channels from shots that
55+
are currently enabled or have been previously enabled. This ensures runviewer remembers
56+
selected channels, even if they do not exist in the current shot, removing the need for a user
57+
to constantly re-enable channels when switching between different types of experiments.
58+
The configuration of enabled channels can also be saved and loaded from the ‘File’ menu,
59+
which is a useful aid when switching between regularly-used experiments.
60+
61+
.. _fig-overview:
62+
63+
.. figure:: img/runviewer_overview.png
64+
65+
The runviewer interface consists of 3 main sections. (1) Controls for loading
66+
shots, selecting the colour of traces, selecting whether shutter open/close markers are to
67+
be shown, and whether the output traces from this shot should be shown. Note that we
68+
have only enabled shutter markers for one of the two shots loaded (the black trace). (2) A
69+
reorderable list of channels contained within the loaded shots. The order here determines
70+
the order of plots in (3). Only enabled channels will be displayed in (3). (3) Plots of the
71+
output traces for the selected channels in the selected shots. Here we show data from 2 shots
72+
of a real experiment sequence from the Monash lab used to study vortex clustering dynamics [2]_.
73+
The two shots loaded demonstrate how you can observe differences in output between shots
74+
in a sequence (in this case due to varying the time between stirring and imaging the vortex
75+
clusters). In this figure we display the entire length of the trace, which makes it difficult
76+
to distinguish between the shutter open/close events (red and green dashed, vertical lines)
77+
and the digital output trace. The discrepancy between these events becomes more apparent
78+
when zooming in (see :numref:`fig-detail`).
79+
80+
The third section comprises the plotting region. We use the Python plotting library
81+
:doc:`pyqtgraph <pyqtgraph:index>` to generate the plots. This choice was primarily made due to the performance
82+
of pyqtgraph, which is significantly faster than other common Python plotting libraries
83+
such as matplotlib. [3]_ The user can pan and zoom the plots produced by pyqtgraph using
84+
the mouse (by holding left or right mouse button respectively while moving the mouse).
85+
The time axes of each plot are linked together so that multiple output traces can be easily
86+
compared to each other. Two buttons are then provided at the top of the interface for
87+
resetting the axes to the default scaling.
88+
89+
As discussed previously, the output traces are generated directly from the hardware
90+
instructions. This creates two problems: information about the timing of certain events
91+
may not be contained within the hardware instructions, and the output trace may contain
92+
too many data points to plot efficiently (even when using pyqtgraph). The first problem
93+
we solve by plotting vertical markers at points of interest. For example, the Shutter class
94+
automatically accounts for the open and close delay of a shutter. The output trace thus
95+
only captures the time at which the digital output goes high or low and does not capture
96+
when the shutter will be open or closed. Runviewer reverse engineers these missing times
97+
from metadata stored within the hdf5 so that they can be plotted as markers
98+
of interest (see :numref:`fig-detail`).
99+
100+
.. _fig-detail:
101+
102+
.. figure:: img/runviewer_detail.png
103+
104+
Here we show the same traces as in :numref:`fig-overview`, but zoomed just after the
105+
22 s mark. We can now clearly see the difference between the change in digital state (black
106+
trace) used to open and close the shutter, and the time at which the shutter was actually
107+
commanded to open and close (green and red dashed, vertical lines respectively). In this
108+
case, the shutter (open, close) delay was specified in the labscript file as (3.11, 2.19) ms for
109+
the central_MOT_shutter and (3.16, 1.74) ms for the science_bottom_imaging_shutter.
110+
111+
The second problem is solved by dynamically resampling the output traces depending
112+
on the zoom level of the x-axis of the plots. I wrote a feature-preserving algorithm for
113+
this purpose to avoid the many down-sampling algorithms that miss features faster than
114+
the sampling rate. This ensures that zoomed out plots accurately represent the trace, even
115+
when resampled. The algorithm starts by creating an output array of points that is 3 times
116+
the maximum width, in pixels, that the plot is expected to be displayed at. We fill every
117+
third data point in the output array using ‘nearest neighbour on the left’ interpolation, using
118+
only the section of the output trace that is currently visible. We then fill the other two data
119+
points with the highest and lowest value between the first data point and the 4th data
120+
point (which will also be determined using ‘nearest neighbour on the left’ interpolation).
121+
These two data point are placed in the order in which they appear, the reason for which
122+
will become clear shortly. This is repeated until the output array is full. The output array
123+
is then passed to pyqtgraph for plotting. Fast features thus exist in three data points of
124+
the array, which pyqtgraph correctly plots in one pixel as a vertical line. This is similar to
125+
the way digital oscilloscopes display acquired signals.
126+
127+
Despite our optimisation efforts, resampling still takes a significant period of time, particularly
128+
if there are many plots displayed. We thus perform the resampling in a thread in
129+
order to keep the GUI responsive. However, because the resampled data has more points
130+
than can be displayed, and these points are in the correct order, zooming in still immediately
131+
shows a reasonable approximation of the trace while the user waits for the resampling
132+
to complete in the background.
133+
134+
.. rubric:: Footnotes
135+
136+
.. [1] Documentation taken from Phillip T. Starkey *A software framework for control and automation of precisely timed experiments*
137+
Thesis, Monash University (2019) https://doi.org/10.26180/5d1db8ffe29ef
138+
139+
.. [2] S. P. Johnstone, A. J. Groszek, P. T. Starkey, C. J. Billington, T. P. Simula, and
140+
K. Helmerson. *Evolution of large-scale flow from turbulence in a two-dimensional
141+
superfluid* Science **364**, 1267 (2019) https://doi.org/10.1126/science.aat5793
142+
143+
.. [3] We typically use matplotlib in the labscript suite as it is a widely known package with an almost
144+
identical syntax to MATLAB. This means that many users are already familiar with the syntax needed
145+
to create plots. As the user is not required to write or modify the code that generates the plots
146+
in runviewer, this benefit was not applicable and so it was worth using pyqtgraph for the increased
147+
performance.

0 commit comments

Comments
 (0)