Basic, Part C: Step Scan with Scaler and Motor#

From APS Python Training for Bluesky Data Acquisition.

Objective

Step scan of scaler and motor with table and plot.

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 gp:.

A docker container is available to provide this IOC. See this URL for instructions: prjemian/epics-docker

Starting with the configuration from the basic scaler and motor notebooks, we first group the imports 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

Next, make a RunEngine (for scanning) and connect our motor and scaler

[2]:
RE = RunEngine({})

P = "gp:"
m1 = EpicsMotor(f"{P}m1", name="m1")
scaler = ScalerCH(f"{P}scaler1", name="scaler")

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()
scaler.read()
[3]:
OrderedDict([('clock', {'value': 16000000.0, 'timestamp': 1684257328.660073}),
             ('I0', {'value': 7.0, 'timestamp': 1684257328.660073}),
             ('scint', {'value': 7.0, 'timestamp': 1684257328.660073}),
             ('scaler_time', {'value': 1.6, 'timestamp': 1684257328.660073})])

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

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]))


+-----------+------------+------------+------------+-------------+------------+
|   seq_num |       time |         I0 |      clock | scaler_time |      scint |
+-----------+------------+------------+------------+-------------+------------+
|         1 | 12:16:30.3 |          1 |    4000000 |       0.400 |          2 |
|         2 | 12:16:31.0 |          2 |    5000000 |       0.500 |          1 |
|         3 | 12:16:31.5 |          3 |    5000000 |       0.500 |          3 |
|         4 | 12:16:32.0 |          2 |    5000000 |       0.500 |          2 |
|         5 | 12:16:32.5 |          1 |    5000000 |       0.500 |          3 |
+-----------+------------+------------+------------+-------------+------------+
generator count ['3b77f60f'] (scan num: 1)


[5]:
('3b77f60f-deab-4dd3-8ae1-cde3f40e90e1',)

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 the basic_motor lesson, displaying the acquired data in a LiveTable.

[6]:
RE(bp.scan([scaler], m1, 1, 5, 5), LiveTable([m1, scaler]))


+-----------+------------+------------+------------------+------------+------------+-------------+------------+
|   seq_num |       time |         m1 | m1_user_setpoint |         I0 |      clock | scaler_time |      scint |
+-----------+------------+------------+------------------+------------+------------+-------------+------------+
|         1 | 12:16:39.0 |     1.0000 |           1.0000 |          2 |    5000000 |       0.500 |          1 |
|         2 | 12:16:40.8 |     2.0000 |           2.0000 |          1 |    5000000 |       0.500 |          3 |
|         3 | 12:16:42.6 |     3.0000 |           3.0000 |          2 |    5000000 |       0.500 |          2 |
|         4 | 12:16:44.4 |     4.0000 |           4.0000 |          1 |    5000000 |       0.500 |          3 |
|         5 | 12:16:46.3 |     5.0000 |           5.0000 |          2 |    5000000 |       0.500 |          2 |
+-----------+------------+------------+------------------+------------+------------+-------------+------------+
generator scan ['08cc8fdd'] (scan num: 2)


[6]:
('08cc8fdd-c541-4905-aa21-974024e0ae66',)

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 inline

We’ll import the LivePlot callback from the Bluesky library:

[8]:
from bluesky.callbacks import LivePlot

Count the scaler 5 times. We’ll just plot the scint signal.

[9]:
RE(bp.count([scaler],num=5), LivePlot("scint"))
[9]:
('0890967d-f1fb-4364-a6f4-8a924b9d4fd9',)
../_images/tutor__basic_c_18_1.png

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"))
[10]:
('5d3b0b3e-8d1d-49af-b1d2-6e07cea61d99',)
../_images/tutor__basic_c_20_1.png

Both the table and the plot are very useful diagnostics for routine use. They have been combined in the Best-Efforts Callback which provides 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
bec = BestEffortCallback()

Count the scaler 5 times:

[12]:
RE(bp.count([scaler], num=5), bec)


Transient Scan ID: 5     Time: 2023-05-16 12:18:51
Persistent Unique Scan ID: '519c4326-fd1b-489a-a951-00ee64f74dcf'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |      clock |         I0 |      scint |
+-----------+------------+------------+------------+------------+
|         1 | 12:18:52.1 |    5000000 |          2 |          2 |
|         2 | 12:18:53.0 |    5000000 |          3 |          2 |
|         3 | 12:18:53.8 |    5000000 |          3 |          2 |
|         4 | 12:18:54.6 |    5000000 |          2 |          2 |
|         5 | 12:18:55.3 |    5000000 |          2 |          2 |
+-----------+------------+------------+------------+------------+
generator count ['519c4326'] (scan num: 5)



[12]:
('519c4326-fd1b-489a-a951-00ee64f74dcf',)
../_images/tutor__basic_c_24_2.png

You might 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), bec)


Transient Scan ID: 6     Time: 2023-05-16 12:19:05
Persistent Unique Scan ID: '50711505-9676-44d3-adf6-a176390b4995'
New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+
|   seq_num |       time |         m1 |      clock |         I0 |      scint |
+-----------+------------+------------+------------+------------+------------+
|         1 | 12:19:10.1 |     1.0000 |    5000000 |          2 |          2 |
|         2 | 12:19:12.2 |     2.0000 |    5000000 |          3 |          2 |
|         3 | 12:19:14.3 |     3.0000 |    5000000 |          2 |          1 |
|         4 | 12:19:16.3 |     4.0000 |    5000000 |          3 |          2 |
|         5 | 12:19:18.4 |     5.0000 |    5000000 |          3 |          2 |
+-----------+------------+------------+------------+------------+------------+
generator scan ['50711505'] (scan num: 6)



[13]:
('50711505-9676-44d3-adf6-a176390b4995',)
../_images/tutor__basic_c_26_2.png

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’ll make a new RunEngine and subscribe the BestEffortCallback to it:

[14]:
RE = RunEngine({})
RE.subscribe(bec)
[14]:
0

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))


Transient Scan ID: 1     Time: 2023-05-16 12:19:30
Persistent Unique Scan ID: '0ff73331-444c-4c2d-8056-31ed673961b1'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |      clock |         I0 |      scint |
+-----------+------------+------------+------------+------------+
|         1 | 12:19:31.4 |    5000000 |          2 |          2 |
|         2 | 12:19:32.2 |    5000000 |          1 |          2 |
|         3 | 12:19:33.0 |    5000000 |          2 |          2 |
|         4 | 12:19:33.8 |    5000000 |          1 |          1 |
|         5 | 12:19:34.6 |    5000000 |          2 |          2 |
+-----------+------------+------------+------------+------------+
generator count ['0ff73331'] (scan num: 1)



[15]:
('0ff73331-444c-4c2d-8056-31ed673961b1',)
../_images/tutor__basic_c_31_2.png

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))


Transient Scan ID: 2     Time: 2023-05-16 12:19:42
Persistent Unique Scan ID: '08024823-0c76-4e4d-9f7d-52d9db1b5471'
New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+
|   seq_num |       time |         m1 |      clock |         I0 |      scint |
+-----------+------------+------------+------------+------------+------------+
|         1 | 12:19:47.7 |     1.0000 |    4000000 |          2 |          1 |
|         2 | 12:19:49.7 |     2.0000 |    5000000 |          2 |          1 |
|         3 | 12:19:51.8 |     3.0000 |    5000000 |          2 |          1 |
|         4 | 12:19:53.8 |     4.0000 |    5000000 |          3 |          2 |
|         5 | 12:19:55.9 |     5.0000 |    5000000 |          2 |          2 |
+-----------+------------+------------+------------+------------+------------+
generator scan ['08024823'] (scan num: 2)



[16]:
('08024823-0c76-4e4d-9f7d-52d9db1b5471',)
../_images/tutor__basic_c_33_2.png

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

"""Basic: Step scan with scaler and motor"""

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 inline


RE = RunEngine({})

P = "gp:"
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()
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"))

bec =  BestEffortCallback()
RE(bp.count([scaler], num=5), bec)
RE(bp.scan([scaler], m1, 1, 5, 5), bec)

RE = RunEngine({})
RE.subscribe(bec)

RE(bp.count([scaler], num=5))
RE(bp.scan([scaler], m1, 1, 5, 5))