Lesson 1: scaler and count#

In this lesson, we’ll work with a scaler (devices that counts pulses emitted from one or more pulse-emitting detector electronics) and use this to make a first lesson in using Bluesky and related tools.

First, we’ll show how to start a Jupyter notebook. Next, we’ll connect with an EPICS scaler (using ophyd), and then use the Bluesky software to count from the scaler.

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 this session in a Jupyter notebook#

This session was started from the linux command line:

jemian@otz ~ $ source /APSshare/anaconda3/Bluesky/bin/activate
(base) jemian@otz ~ $ jupyter-notebook

This command produced the following console output and then started my default web browser with a one-time-token-authenticated connection to the Jupyter Notebook server (still running in the console):

[I 15:16:57.546 NotebookApp] Serving notebooks from local directory: /home/oxygen18/JEMIAN
[I 15:16:57.546 NotebookApp] 0 active kernels
[I 15:16:57.546 NotebookApp] The Jupyter Notebook is running at:
[I 15:16:57.546 NotebookApp] http://localhost:8888/?token=e6a7584762c731a7c64f8f71246b3e616d779f7b4852c9d9
[I 15:16:57.546 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 15:16:57.547 NotebookApp]

    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=e6a7584762c731a7c64f8f71246b3e616d779f7b4852c9d9
[I 15:17:00.863 NotebookApp] Accepting one-time-token-authenticated connection from ::1

Next, found the New drop-down menu button (top right, below the Lougout button) and selected Python 3 to start a new notebook page using a Python 3 shell (the only kind available here).

Finally, from the File menu in the jupyter notebook (in the browser), selected Rename … to save the Untitled notebook with the name lesson1 (default extension is .ipynb).

Connect an EPICS scaler#

I have a synApps (v6.1) XXX-style IOC with the prefix sky:. It has a scaler, 16 soft channel motors, and some other support we’ll ignore in lesson 1.

The scaler is sky:scaler1. We’ll connect to that first. To make the connection, we need to import the ScalerCH device from the ophyd.scaler library.

[1]:
from ophyd.scaler import ScalerCH
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 from ophyd.scaler import ScalerCH

ModuleNotFoundError: No module named 'ophyd'

Now we can create the scaler object we’ll use as a detector.

[2]:
scaler = ScalerCH("sky:scaler1", name="scaler")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 scaler = ScalerCH("sky:scaler1", name="scaler")

NameError: name 'ScalerCH' is not defined

In a script or program, we should wait for that to connect with EPICS.

[3]:
scaler.wait_for_connection()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 scaler.wait_for_connection()

NameError: name 'scaler' is not defined

Let’s test that connection by asking the scaler to read its values from EPICS.

[4]:
scaler.read()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 scaler.read()

NameError: name 'scaler' is not defined

By default, the ophyd ScalerCH object shows data for only the channels with names defined in EPICS. Except for one channel (probably a bug in the ScalerCH code). To fix that, we’ll load another support library. (Loading additional support as we need it will be a common theme in these lessons. Rather than loading all the libraries first, as is common in python code files, we’ll load support code as the need arises.)

The new support code is use_EPICS_scaler_channels from apstools.devices.

Our scaler only has a few channels in use (that is, channels where the name of the channel has been specified in the GUI screen). Let’s focus on just those channels.

To read just the named channels, we call the scaler’s select_channels() method and name the scaler channels we wish to see. To see all channels with names defined in EPICS, use None as shown:

[5]:
scaler.select_channels(None)
scaler.read()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 scaler.select_channels(None)
      2 scaler.read()

NameError: name 'scaler' is not defined

If you want to change the name fields on any of the scaler channels from the command line, follow this example.

[6]:
scaler.channels.chan04.chname.put("scint")
scaler.channels.chan07.chname.put("roi1")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 scaler.channels.chan04.chname.put("scint")
      2 scaler.channels.chan07.chname.put("roi1")

NameError: name 'scaler' is not defined

Then, update the scaler object for these channels.

[7]:
scaler.select_channels(None)
scaler.read()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 scaler.select_channels(None)
      2 scaler.read()

NameError: name 'scaler' is not defined

Get, then Set the count time on the scaler#

The preset counting time (.TP) is one of the many fields of the EPICS scaler record. It is available as scaler.preset_time in this scaler object. We can inspect this:

[8]:
scaler.preset_time
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 scaler.preset_time

NameError: name 'scaler' is not defined

Since scaler.preset_time is an EpicsSignal, (Signal is a fundamental ophyd structure), we print its .value.

[9]:
scaler.preset_time.get()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 scaler.preset_time.get()

NameError: name 'scaler' is not defined

Set the scaler to count for 1.5 seconds once it is told to count. We’ll count it next.

[10]:
scaler.preset_time.put(1.5)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 scaler.preset_time.put(1.5)

NameError: name 'scaler' is not defined

Note, to tell the scaler to count, we put a 1 to its .CNT field.

[11]:
scaler.count.put(1)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 1
----> 1 scaler.count.put(1)

NameError: name 'scaler' is not defined

Now, read the scaler again. This scaler is not very interesting since it is not connected to any hardware. But, the timestamps have changed.

[12]:
scaler.read()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 scaler.read()

NameError: name 'scaler' is not defined

Use Bluesky to count the scaler#

So far, we have used ophyd to talk with the EPICS scaler. Let’s start to use Bluesky. We need to load support to start the Bluesky RunEngine. For now, we’ll use the most basic configuration (does not save data anywhere).

The job of the RunEngine is to process command messages (our data acquisition commands) and to output documents (the data to be acquired). Initially, we’ll use predefined data acquisition sequences (called plans) and ignore the documents.

[13]:
from bluesky import RunEngine
RE = RunEngine({})
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[13], line 1
----> 1 from bluesky import RunEngine
      2 RE = RunEngine({})

ModuleNotFoundError: No module named 'bluesky'

Next, we’ll load a library with many predefined plans. The first plan we want to use is count(). By the way, we’ll use a python feature to rename bluesky.plans to the shorter bp since it will be used a lot.

[14]:
import bluesky.plans as bp
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[14], line 1
----> 1 import bluesky.plans as bp

ModuleNotFoundError: No module named 'bluesky'

Now we can count the scaler by submitting the bp.count plan to the RunEngine instance RE. The bp.count() plan takes one argument, a list of countable objects (just in case you want to count more than one detector at the same time). Our list has only one object: scaler

[15]:
RE(bp.count([scaler]))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 1
----> 1 RE(bp.count([scaler]))

NameError: name 'RE' is not defined

The final result from submitting a plan to the RunEngine is an identifier of the sequence of documents. Since we did not capture that sequence, we can’t view it or access it in any way. Let’s fix that next.

Report about the documents emitted from the RunEngine#

To display information about the documents emitted from the RunEngine, we need to make a function that will receive the documents.

In Bluesky terms, this type of function is a callback. It takes two arguments. The first argument is a str that tells what kind of document is coming, the second is a python dictionary with the document’s contents. We’ll start by printing summary information. We then submit the plan again with the name of our callback function as a second argument (not to bp.count but as a second argument to RE()).

[16]:
def myCallbackBrief(key, doc):
    print(key, len(doc))
[17]:
RE(bp.count([scaler]), myCallbackBrief)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 RE(bp.count([scaler]), myCallbackBrief)

NameError: name 'RE' is not defined

So, there were four documents. See https://blueskyproject.io/bluesky/documents.html for the details of each. Let’s extend our callback to print the details of each.

[18]:
def myCallback(key, doc):
    print(key, len(doc))
    for k, v in doc.items():
        print("\t", k, v)
    print("~~~~~~~~~~~~~~~~~")
[19]:
RE(bp.count([scaler]), myCallback)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 RE(bp.count([scaler]), myCallback)

NameError: name 'RE' is not defined

With the details, we see more of what is happening.

The first document is a start and it says what is happening and how it will be identified. The identifier (named uid) is a UUID (specifically, a uuid4). This looks tedious and random. The one good thing is that these are unique and often can be referred to by the first few characters (6-8 are enough to be probably unique). The uid from the start document is what the RunEngine reports when the scan stops.

The second document is a descriptor and it says what, exactly, will be measured. It also includes a reference to start’s uid. We can follow the chain back to start this way.

The third document is an event and it provides the data reading when the scaler was counted. The data is read according to the descriptor document (just actual values for the named channels).

The last document is a stop document that describes how things ended. It also says there was only one stream of event documents named primary.

Let’s repeat that plan, this time asking to count the scaler three times. This is the num=3 addition to the call to bp.count(). What’s tricky now is keeping track of the parentheses and square braces.

[20]:
RE(bp.count([scaler], num=3), myCallback)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[20], line 1
----> 1 RE(bp.count([scaler], num=3), myCallback)

NameError: name 'RE' is not defined

Mostly the same but now there are three event documents.

Summary#

We’ll show this code as a python program:

#!/usr/bin/env python

"lesson 1: EpicsScaler"

from ophyd.scaler import ScalerCH
from bluesky import RunEngine
import bluesky.plans as bp
from apstools.devices import use_EPICS_scaler_channels


def myCallback(key, doc):
    print(key, len(doc))
    for k, v in doc.items():
        print("\t", k, v)
    print("~~~~~~~~~~~~~~~~~")


RE = RunEngine({})

scaler = ScalerCH("sky:scaler1", name="scaler")
scaler.preset_time.put(1.5)
print(scaler.preset_time.value)

scaler.channels.chan04.chname.put("scint")
scaler.channels.chan07.chname.put("roi1")

scaler.match_names()
use_EPICS_scaler_channels(scaler)
print(scaler.read())
print(RE(bp.count([scaler], num=3), myCallback))

Quit the jupyter notebook#

  • Save the “lesson1” page if you wish.

  • Press the Logout button in the upper right corner of “lesson1” and close the page.

  • Press the Logout button in the upper right corner of “Home” and close the page.

  • Type Control-C (^C) twice in the jupyter-notebook console to kill the server.