Lesson 3, Part A: Show the data as it is acquired#
In this lesson, we’ll show how to use the tools provided with Bluesky to show the data as it is acquired using both a table representation and a graphical view, as well. These capabilities are provided by using callbacks. In lessons 1 and 2, we wrote our own simple callback to view the documents that come from the RunEngine during execution of a plan. Quickly, the data became too complex for simple viewing.
The LiveTable and LivePlot callbacks provide a table and graphical view of the data from the plan. We’ll get to those first. Later, we’ll show the BestEffortCallback, which combines both those callbacks plus a little more. For routine work, we’ll want to use BestEffortCallback all the time. We’ll show how to make that happen so we set it and forget about it.
note: This tutorial expects to find an EPICS IOC on the local network configured as a synApps xxx IOC with prefix sky:
. A docker container is available to provide this IOC. See this URL for instructions: prjemian/epics-docker
Starting with the configuration from lessons 1 and 2, we first group the import
s together as is common Python practice:
[1]:
from ophyd import EpicsMotor
from ophyd.scaler import ScalerCH
from bluesky import RunEngine
import bluesky.plans as bp
from apstools.devices import use_EPICS_scaler_channels
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[1], line 1
----> 1 from ophyd import EpicsMotor
2 from ophyd.scaler import ScalerCH
3 from bluesky import RunEngine
ModuleNotFoundError: No module named 'ophyd'
Next, make a RunEngine (for scanning) and connect our motor and scaler
[2]:
RE = RunEngine({})
P = "sky:"
m1 = EpicsMotor(f"{P}m1", name="m1")
scaler = ScalerCH(f"{P}scaler1", name="scaler")
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[2], line 1
----> 1 RE = RunEngine({})
3 P = "sky:"
4 m1 = EpicsMotor(f"{P}m1", name="m1")
NameError: name 'RunEngine' is not defined
Reconfigure the scaler for channel names, set the counting time to 0.5 s, and read the scaler values.
[3]:
scaler.channels.chan01.chname.put("clock")
scaler.channels.chan02.chname.put("I0")
scaler.channels.chan03.chname.put("scint")
scaler.preset_time.put(0.4)
scaler.select_channels(None)
scaler.read()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 scaler.channels.chan01.chname.put("clock")
2 scaler.channels.chan02.chname.put("I0")
3 scaler.channels.chan03.chname.put("scint")
NameError: name 'scaler' is not defined
Showing the data#
In lessons 1 and 2, we wrote a callback routine that printed information as the scan prpogressed (that is, we printed select content from the stream of documents emitted by the RunEngine while executing a plan). But our callback was simple and we found there is a lot of content in the documents from the RunEngine.
The simplest example of a Bluesky callback is the print
function. We want a callback function that understands our data and uses reasonable assumptions to show that data as it is being acquired.
One method to display our data is in a table that updates as the scan progresses. We’ll import the LiveTable
callback from the Bluesky library:
[4]:
from bluesky.callbacks import LiveTable
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[4], line 1
----> 1 from bluesky.callbacks import LiveTable
ModuleNotFoundError: No module named 'bluesky'
LiveTable()
shows acquired data as a plan is executed. The argument is the list of detectors to show in the table. First, we’ll count the scaler 5 times.
[5]:
RE(bp.count([scaler], 5), LiveTable([scaler]))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[5], line 1
----> 1 RE(bp.count([scaler], 5), LiveTable([scaler]))
NameError: name 'RE' is not defined
You see columns for the data collection sequence number, the time of collection, and each of the named scaler channels. At the end, the short form for the scan’s uid
is shown as well as scan num
which is a more convenient reference to the scan. The user has control to set or reset scan num
so do not rely on that number to be unique.
Next, we’ll scan with motor and scaler, as we did in lesson 2, displaying the acquired data in a LiveTable
.
[6]:
RE(bp.scan([scaler], m1, 1, 5, 5), LiveTable([m1, scaler]))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[6], line 1
----> 1 RE(bp.scan([scaler], m1, 1, 5, 5), LiveTable([m1, scaler]))
NameError: name 'RE' is not defined
In addition to the data columns from count
above, the motor position (both where the motor reported as its position and where the motor was told to go, respectively) are shown.
There is a callback routine that will plot the data as it is acquired. When starting graphics, it is necessary to first initialize the graphics manager of the display. The setup is specific to the graphics manager. For command line or python program use, see https://blueskyproject.io/bluesky/callbacks.html#aside-making-plots-update-live.
For jupyter notebooks:
[7]:
%matplotlib notebook
from bluesky.utils import install_nb_kicker
install_nb_kicker()
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[7], line 1
----> 1 get_ipython().run_line_magic('matplotlib', 'notebook')
2 from bluesky.utils import install_nb_kicker
3 install_nb_kicker()
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/IPython/core/interactiveshell.py:2480, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
2478 kwargs['local_ns'] = self.get_local_scope(stack_depth)
2479 with self.builtin_trap:
-> 2480 result = fn(*args, **kwargs)
2482 # The code below prevents the output from being displayed
2483 # when using magics with decorator @output_can_be_silenced
2484 # when the last Python token in the expression is a ';'.
2485 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/IPython/core/magics/pylab.py:103, in PylabMagics.matplotlib(self, line)
98 print(
99 "Available matplotlib backends: %s"
100 % _list_matplotlib_backends_and_gui_loops()
101 )
102 else:
--> 103 gui, backend = self.shell.enable_matplotlib(args.gui)
104 self._show_matplotlib_backend(args.gui, backend)
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3665, in InteractiveShell.enable_matplotlib(self, gui)
3662 import matplotlib_inline.backend_inline
3664 from IPython.core import pylabtools as pt
-> 3665 gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)
3667 if gui != None:
3668 # If we have our first gui selection, store it
3669 if self.pylab_gui_select is None:
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/IPython/core/pylabtools.py:338, in find_gui_and_backend(gui, gui_select)
321 def find_gui_and_backend(gui=None, gui_select=None):
322 """Given a gui string return the gui and mpl backend.
323
324 Parameters
(...)
335 'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg').
336 """
--> 338 import matplotlib
340 if _matplotlib_manages_backends():
341 backend_registry = matplotlib.backends.registry.backend_registry
ModuleNotFoundError: No module named 'matplotlib'
We’ll import the LivePlot
callback from the Bluesky library:
[8]:
from bluesky.callbacks import LivePlot
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[8], line 1
----> 1 from bluesky.callbacks import LivePlot
ModuleNotFoundError: No module named 'bluesky'
Count the scaler 5 times. We’ll just plot the scint
signal.
[9]:
RE(bp.count([scaler],num=5), LivePlot("scint"))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[9], line 1
----> 1 RE(bp.count([scaler],num=5), LivePlot("scint"))
NameError: name 'RE' is not defined
To scan, we need to tell LivePlot
to plot `scint’ vs. the motor:
[10]:
RE(bp.scan([scaler], m1, 1, 5, 5), LivePlot("scint", "m1"))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[10], line 1
----> 1 RE(bp.scan([scaler], m1, 1, 5, 5), LivePlot("scint", "m1"))
NameError: name 'RE' is not defined
Both the table and the plot are very useful diagnostics for routine use. They have been combined in the Best-Efforts Callback which provides best-effort plots and visualization for any plan. It uses user-configurable information that is part of every ophyd device to make reasonable assumptions about what information is appropriate to display in the context of the current plan.
We’ll import the BestEffortCallback
callback from the Bluesky library:
[11]:
from bluesky.callbacks.best_effort import BestEffortCallback
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[11], line 1
----> 1 from bluesky.callbacks.best_effort import BestEffortCallback
ModuleNotFoundError: No module named 'bluesky'
Count the scaler 5 times:
[12]:
RE(bp.count([scaler], num=5), BestEffortCallback())
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[12], line 1
----> 1 RE(bp.count([scaler], num=5), BestEffortCallback())
NameError: name 'RE' is not defined
You see both the LiveTable
and the LivePlot
output tangled up here in the jupyter notebook. Each is created on demand and then updated as the plan progresses. When executing in a command line environment, the LivePlot
is shown in a separate window.
Repeat the same scan, noting that we do not need to inform the callback what to display:
[13]:
RE(bp.scan([scaler], m1, 1, 5, 5), BestEffortCallback())
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[13], line 1
----> 1 RE(bp.scan([scaler], m1, 1, 5, 5), BestEffortCallback())
NameError: name 'RE' is not defined
Because this is such a useful tool, we want to make this callback happen all the time. The RunEngine manages a list of such callbacks. We subscribe the BestEffortCallback
:
[14]:
RE.subscribe(BestEffortCallback())
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[14], line 1
----> 1 RE.subscribe(BestEffortCallback())
NameError: name 'RE' is not defined
Repeat the count of the scaler (without adding the callback in the command):
In jupyter notebook, we can see the LiveTable
after our scan command. To see the LivePlot
, we have to look up a few cells, where the plots of scaler channels vs. m1
are shown, our latest data identified by scan num
in the legend.RE(bp.count([scaler], num=5))
[15]:
RE(bp.count([scaler], num=5))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[15], line 1
----> 1 RE(bp.count([scaler], num=5))
NameError: name 'RE' is not defined
In jupyter notebook, we can see the LiveTable
after our count command. To see the LivePlot
, we have to look up a few cells, where the plots of scaler channels vs. time are shown, our latest data identified by scan num
in the legend.
Then, repeat the scan (again, without adding the callback in the command):
[16]:
RE(bp.scan([scaler], m1, 1, 5, 5))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[16], line 1
----> 1 RE(bp.scan([scaler], m1, 1, 5, 5))
NameError: name 'RE' is not defined
In jupyter notebook, we can see the LiveTable
after our scan command. Notice that the number of columns displayed is less than when we called LiveTable
ourselves. To see the LivePlot
, we have to look up a few cells, where the plots of scaler channels vs. m1
are shown, our latest data identified by scan num
in the legend.
Summary#
We’ll show this code as a python program:
#!/usr/bin/env python
"lesson 3: Show the data as it is acquired"
from ophyd import EpicsMotor
from ophyd.scaler import ScalerCH
from bluesky import RunEngine
import bluesky.plans as bp
from bluesky.callbacks import LiveTable
from bluesky.callbacks import LivePlot
from bluesky.callbacks.best_effort import BestEffortCallback
from apstools.devices import use_EPICS_scaler_channels
%matplotlib notebook
from bluesky.utils import install_qt_kicker
install_qt_kicker()
RE = RunEngine({})
P = "sky:"
m1 = EpicsMotor(f"{P}m1", name="m1")
scaler = ScalerCH(f"{P}scaler1", name="scaler")
m1.wait_for_connection()
scaler.wait_for_connection()
scaler.preset_time.put(0.4)
scaler.select_channels(None)
print(scaler.read())
RE(bp.count([scaler], num=5), LiveTable([scaler]))
RE(bp.scan([scaler], m1, 1, 5, 5), LiveTable([m1, scaler]))
RE(bp.count([scaler], num=5), LivePlot("scint"))
RE(bp.scan([scaler], m1, 1, 5, 5), LivePlot("scint", "m1"))
RE(bp.count([scaler], num=5), BestEffortCallback())
RE(bp.scan([scaler], m1, 1, 5, 5), BestEffortCallback())
RE.subscribe(BestEffortCallback())
RE(bp.count([scaler], num=5))
RE(bp.scan([scaler], m1, 1, 5, 5))