Minimal: Scan scaler vs motor#

APS Training for Bluesky Data Acquisition.

Objective

Connect an EPICS motor PV, an EPICS scaler PV, and scan using the bluesky RunEngine with the BestEffortCallback to visualize the acquired data.

EPICS PV

Python object name

description

gp:m1

xpos

motor PV (simulates a stepper motor)

gp:scaler1

vsc16

scaler PV (simulates a Joerger VSC16 scaler)

Connect the motor#

First, import the ophyd.EpicsMotor (class) definition. This Python class from ophyd is the best representation of the synApps motor record.

[1]:
from ophyd import EpicsMotor

Then, define the xpos object. The EPICS PV is the first argument. The name="xpos" is required. Make the value of this keyword argument match the name of the Python object being assigned. The labels=("motor",) keyword argument enables certain features for the user interface that will be described later.

[2]:
xpos = EpicsMotor("gp:m1", name="xpos", labels=("motor",))

Wait for that to connect with EPICS

[3]:
xpos.wait_for_connection()

Show the position of the xpos motor now.

[4]:
xpos.position
[4]:
-0.3

Try to move xpos using the IPython magic command: %mov.

[5]:
# wrap in a try..except handler so notebook can run past this point
try:
    %mov xpos 1
except Exception as exc:
    print(f"Exception: {exc}")
Exception: Line magic function `%mov` not found.

Note this command fails since the IPython magic commands are not loaded automatically. As an alternative, use the motor’s .move(SETPOINT) method to move the motor to the new SETPOINT value. Here, move xpos to 1.

[6]:
xpos.move(1)
[6]:
MoveStatus(done=True, pos=xpos, elapsed=1.5, success=True, settle_time=0.0)

The .move() method returns a status object. The status object may be used to report about the move or to wait for the motor to complete its move.

Next, load the IPython Magic commands provided by bluesky. These are helpers to make the command line use easier. Use them at your choice.

[7]:
from bluesky.magics import BlueskyMagics
get_ipython().register_magics(BlueskyMagics)

Repeat the move of xpos by sending it to 0 using the magic command.

[8]:
%mov xpos 0
xpos:   9%|██▌                         | 0.09/1.0 [00:00<00:01,  1.46s/degrees]
xpos:  19%|█████▎                      | 0.19/1.0 [00:00<00:00,  1.22s/degrees]
xpos:  29%|███████▊                   | 0.291/1.0 [00:00<00:00,  1.15s/degrees]
xpos:  39%|██████████▌                | 0.391/1.0 [00:00<00:00,  1.11s/degrees]
xpos:  49%|█████████████▎             | 0.491/1.0 [00:00<00:00,  1.09s/degrees]
xpos:  59%|███████████████▉           | 0.592/1.0 [00:00<00:00,  1.07s/degrees]
xpos:  69%|██████████████████▋        | 0.692/1.0 [00:00<00:00,  1.06s/degrees]
xpos:  79%|█████████████████████▍     | 0.793/1.0 [00:00<00:00,  1.05s/degrees]
xpos:  89%|████████████████████████▏  | 0.894/1.0 [00:00<00:00,  1.05s/degrees]
xpos:  97%|███████████████████████████▏| 0.97/1.0 [00:01<00:00,  1.07s/degrees]
xpos: 100%|██████████████████████████▉| 0.999/1.0 [00:01<00:00,  1.14s/degrees]
xpos: 100%|█████████████████████████████| 1.0/1.0 [00:01<00:00,  1.24s/degrees]
xpos [In progress. No progress bar available.]

Connect the scaler#

The ophyd package provides two representations of the synApps scaler record (EpicsScaler and ScalerCH). My opinion is that the ScalerCH class provides the representation most compatible with use at the APS.

As before, import the ophyd class:

[9]:
from ophyd.scaler import ScalerCH

Then, connect with the EPICS PV. This is similar to how the motor was connected (above). Make the name= keyword match with the Python object name being created. The labels=("scalers", "detectors") keyword argument enables certain features for the user interface that will be described later.

[10]:
vsc16 = ScalerCH("gp:scaler1", name="vsc16", labels=["scalers", "detectors"])

Configure the Python object to ignore the channels with no name defined (in the .NMnn field of the scaler PV).

In this example control screen for our scaler, only a few of the channels are named:

``scaler`` GUI

[11]:
vsc16.select_channels()

Show the value for each of the named channels in our scaler. This will also include the most recent count time.

[12]:
vsc16.read()
[12]:
OrderedDict([('timebase',
              {'value': 6000000.0, 'timestamp': 1629391538.435033}),
             ('I0', {'value': 1.0, 'timestamp': 1629391538.435033}),
             ('scint', {'value': 2.0, 'timestamp': 1629391538.435033}),
             ('diode', {'value': 3.0, 'timestamp': 1629391538.435033}),
             ('I00', {'value': 3.0, 'timestamp': 1629391538.435033}),
             ('roi1', {'value': 0.0, 'timestamp': 1629391538.435033}),
             ('roi2', {'value': 0.0, 'timestamp': 1629391538.435033}),
             ('vsc16_time', {'value': 0.6, 'timestamp': 1629391538.435033})])

If no channels are shown, such as:

OrderedDict([('vsc16_time', {'value': 0.0, 'timestamp': 631152000.0})])

then we want to name some channels here for our simulator. We can do this directly from Python (although you would not usually want to do this at a real, operating instrument, since the channels are named as a result of hardware connections with real detectors). We’ll take channels 1-5 for timebase, I0, scint, diode, & I00, then skip a few channels to 11 & 12 and call them roi1 & roi2, respectively.

It’s OK to have empty (that is, unnamed) channels. It’s not OK to have white space in these names since the names will be used as Python objects within bluesky. That also means no other characters (such as math symbols and such) that cannot be used as a Python object name. Also, do not use the same name in two different scaler channels.

[13]:
vsc16.channels.chan01.chname.put("timebase")
vsc16.channels.chan02.chname.put("I0")
vsc16.channels.chan03.chname.put("scint")
vsc16.channels.chan04.chname.put("diode")
vsc16.channels.chan05.chname.put("I00")
vsc16.channels.chan11.chname.put("roi1")
vsc16.channels.chan12.chname.put("roi2")

Then select the named channels and read again:

[14]:
vsc16.select_channels()
vsc16.read()
[14]:
OrderedDict([('timebase',
              {'value': 6000000.0, 'timestamp': 1629391538.435033}),
             ('I0', {'value': 1.0, 'timestamp': 1629391538.435033}),
             ('scint', {'value': 2.0, 'timestamp': 1629391538.435033}),
             ('diode', {'value': 3.0, 'timestamp': 1629391538.435033}),
             ('I00', {'value': 3.0, 'timestamp': 1629391538.435033}),
             ('roi1', {'value': 0.0, 'timestamp': 1629391538.435033}),
             ('roi2', {'value': 0.0, 'timestamp': 1629391538.435033}),
             ('vsc16_time', {'value': 0.6, 'timestamp': 1629391538.435033})])

Set the scaler’s counting time#

The ScalerCH class defines the count time as the preset_time attribute. Show it’s value:

[15]:
vsc16.preset_time.get()
[15]:
0.5

Set the counting time to 0.5 s using the %mov magic command (same command that moves a motor).

[16]:
%mov vsc16.preset_time 0.5

Use the IPython %ct magic command to count each object with the detectors label keyword.

[17]:
vsc16.preset_time.get()
[17]:
0.5
[18]:
ct
[This data will not be saved. Use the RunEngine to collect data.]
timebase                       6000000.0
I0                             2.0
scint                          3.0
diode                          2.0
I00                            2.0
roi1                           0.0
roi2                           0.0
vsc16_time                     0.6
[19]:
%ct
[This data will not be saved. Use the RunEngine to collect data.]
timebase                       6000000.0
I0                             3.0
scint                          2.0
diode                          3.0
I00                            2.0
roi1                           0.0
roi2                           0.0
vsc16_time                     0.6

labels and the %wa magic command#

As noted above, the labels=LIST keyword argument used when the motor and scaler objects were create enable certain features. For example, the %ct magic command will count all the detectors.

Actually, detectors is the default argument for the %ct magic. If an arument is supplied, it is the name of a label to be matched. This is why the additional label of scalers was included. Thus, we could count only scalers with the command %ct scalers.

Use the labels keyword liberally to group similar objects.

The %wa magic shows relevant information for all labeled objects (or, for the named label if supplied). For example:

[20]:
%wa
motor
  Positioner                     Value       Low Limit   High Limit  Offset
  xpos                           0.0         -32000.0    32000.0     0.0

  Local variable name                    Ophyd name (to be recorded as metadata)
  xpos                                   xpos

scalers
  Local variable name                    Ophyd name (to be recorded as metadata)
  vsc16                                  vsc16

detectors
  Local variable name                    Ophyd name (to be recorded as metadata)
  vsc16                                  vsc16

[21]:
%wa motor
motor
  Positioner                     Value       Low Limit   High Limit  Offset
  xpos                           0.0         -32000.0    32000.0     0.0

  Local variable name                    Ophyd name (to be recorded as metadata)
  xpos                                   xpos

Prepare to scan#

Before we can run our first bluesky scan, we have to import various software tools.

First is the bluesky RunEngine(), which will manage the various activities for the scan (move motor, wait, trigger scaler, wait, collect channel data, publish to data subscribers, …).

Create a RunEngine() object. The argument here is a dictionary (empty in this training session). (For routine operations at an instrument, the dictionary is filled with information saved from the previous session.)

[22]:
import bluesky
RE = bluesky.RunEngine({})

To save the acquired data, we connect with a MongoDB database using a preconfigured datafile that describes our training catalog. Using the IPython ! technique to issue a Linux command from an IPython session, we cat (concatenate) the contents of that file to the output here:

[23]:
!cat ~/.local/share/intake/training.yml
# file: training.yml
# purpose: Configuration file to connect Bluesky databroker with MongoDB
# For Bluesky Python Training at APS

# Copy to: ~/.local/share/intake/training.yml
# Create subdirectories as needed

sources:
  training:
    args:
      asset_registry_db: mongodb://localhost:27017/training-bluesky
      metadatastore_db: mongodb://localhost:27017/training-bluesky
    driver: bluesky-mongo-normalized-catalog

To connect, we need the training catalog. This name is provided by the line indented after the sources: line in the above .yml file. (The name of the .yml file does not matter. The databroker.catalog software will look through all .yml files in this directory for thetraining configuration.)

We’ll use the reference to our databroker catalog frequently, so we give it a short name.

[24]:
import databroker
db = databroker.catalog["training"]

How many (bluesky data collection) runs are recorded in this catalog? Get its length:

[25]:
len(db)
[25]:
68

Configure RE to publish the run data to our db object. We must use the .v1 software interface for legacy reasons.

[26]:
RE.subscribe(db.v1.insert)
[26]:
0

A progress bar can be helpful to show that long operations are actually progressing. These steps load a progress bar and configure RE.

[27]:
from bluesky.utils import ProgressBarManager
pbar_manager = ProgressBarManager()
RE.waiting_hook = pbar_manager

The BestEffortCallback provides easy visualization of data (tables, plots, peaks statistics) as it is acquired by the RE. Subscribe it to the RE so it receives data during a RE() run.

[28]:
from bluesky.callbacks.best_effort import BestEffortCallback
bec = BestEffortCallback()
RE.subscribe(bec)
peaks = bec.peaks

Supplemental baseline data is recorded before and after each run. Additionally, EPICS Channel Access monitors can update PVs asynchronous to the primary data acquisition. These monitors can be saved as additional data streams in a run. Prepare to use this feature.

[29]:
from bluesky import SupplementalData
sd = SupplementalData()
RE.preprocessors.append(sd)

Add the name of this notebook as metadata to every run. This is done by adding to the RunEngine’s metadata dictionary (RE.md), content that will be added to the start document of every run. The metadata is useful documentation about a run and can be used for several purposes, such as to record a general condition (such as the name of this notebook) or to identify these runs from a database search.

[30]:
RE.md["notebook"] = "basic-motor-scaler-scan"

Summary of preparations for scanning#

  • EPICS connections to motor and scaler

  • databroker

  • RunEngine

  • subscriptions

    • databroker

    • ProgressBarManager

    • BestEffortCallback (tables and plots)

    • supplemental data (monitors and baselines)

  • metadata

First scan#

The standard plans provided in bluesky.plans are sufficient for many needs, so import them and, in the same command, give the package a short name (bp) since it is used frequently.

[31]:
from bluesky import plans as bp

The scan() plan is flexible and will be used here to scan scaler vs. motor. The first argument is the list of detectors to be recorded. Here, we give the scaler object vsc16. The next argument is the positioner object, then start and end positions, finally, the number of points to be collected.

Observe that we do not run the scan directly, but rather give the scan to the RE() object. The RE() object will run the scan, performing each of the actions defined by the scan, but also handle the additional tasks of managing the data acquisition process, publishing data to all subscribers (here: databroker and BestEffortCallback) and checking for updates from EPICS and checking if the run must be interrupted either by user request or some other observation. (We have not configured any of those other observations in this simple example.)

[32]:
RE(bp.scan([vsc16], xpos, -1, 1, 7))


Transient Scan ID: 1     Time: 2021-08-19 14:02:41
Persistent Unique Scan ID: 'fb41419c-0aef-4ff4-8992-7aa2b62f9931'
xpos:   9%|██▍                        | 0.091/1.0 [00:00<00:01,  1.97s/degrees]
xpos:  19%|█████▎                      | 0.19/1.0 [00:00<00:01,  1.47s/degrees]
xpos:  29%|████████                    | 0.29/1.0 [00:00<00:00,  1.31s/degrees]
xpos:  39%|██████████▉                 | 0.39/1.0 [00:00<00:00,  1.23s/degrees]
xpos:  49%|█████████████▎             | 0.491/1.0 [00:00<00:00,  1.18s/degrees]
xpos:  59%|███████████████▉           | 0.591/1.0 [00:00<00:00,  1.15s/degrees]
xpos:  69%|██████████████████▋        | 0.691/1.0 [00:00<00:00,  1.13s/degrees]
xpos:  79%|█████████████████████▍     | 0.792/1.0 [00:00<00:00,  1.11s/degrees]
xpos:  89%|████████████████████████   | 0.892/1.0 [00:00<00:00,  1.10s/degrees]
xpos:  97%|██████████████████████████▏| 0.968/1.0 [00:01<00:00,  1.12s/degrees]
xpos: 100%|██████████████████████████▉| 0.999/1.0 [00:01<00:00,  1.18s/degrees]
xpos: 100%|█████████████████████████████| 1.0/1.0 [00:01<00:00,  1.28s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
|   seq_num |       time |       xpos |   timebase |         I0 |      scint |      diode |        I00 |       roi1 |       roi2 |
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
|         1 | 14:02:43.7 |   -1.00000 |    6000000 |          3 |          2 |          1 |          3 |          0 |          0 |
xpos:  27%|██████▎                | 0.091/0.33333 [00:00<00:00,  1.79s/degrees]
xpos:  57%|█████████████▋          | 0.19/0.33333 [00:00<00:00,  1.38s/degrees]
xpos:  84%|████████████████████▏   | 0.28/0.33333 [00:00<00:00,  1.29s/degrees]
xpos:  98%|██████████████████████▌| 0.327/0.33333 [00:00<00:00,  1.42s/degrees]
xpos: 100%|██████████████████████▉| 0.333/0.33333 [00:00<00:00,  1.69s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         2 | 14:02:45.8 |   -0.66700 |    6000000 |          3 |          3 |          3 |          2 |          0 |          0 |
xpos:  27%|██████▎                | 0.091/0.33367 [00:00<00:00,  2.07s/degrees]
xpos:  57%|█████████████▏         | 0.191/0.33367 [00:00<00:00,  1.52s/degrees]
xpos:  84%|███████████████████▎   | 0.281/0.33367 [00:00<00:00,  1.39s/degrees]
xpos:  98%|██████████████████████▌| 0.327/0.33367 [00:00<00:00,  1.50s/degrees]
xpos: 100%|█████████████████████| 0.33367/0.33367 [00:00<00:00,  1.77s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         3 | 14:02:48.0 |   -0.33300 |    6000000 |          2 |          2 |          2 |          3 |          0 |          0 |
xpos:  27%|██████▊                  | 0.091/0.333 [00:00<00:00,  1.42s/degrees]
xpos:  57%|██████████████▎          | 0.191/0.333 [00:00<00:00,  1.20s/degrees]
xpos:  84%|█████████████████████▊    | 0.28/0.333 [00:00<00:00,  1.18s/degrees]
xpos:  98%|████████████████████████▌| 0.327/0.333 [00:00<00:00,  1.32s/degrees]
xpos: 100%|█████████████████████████| 0.333/0.333 [00:00<00:00,  1.60s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         4 | 14:02:50.1 |    0.00000 |    6000000 |          1 |          3 |          1 |          2 |          0 |          0 |
xpos:  27%|██████▍                 | 0.09/0.33333 [00:00<00:00,  1.35s/degrees]
xpos:  57%|█████████████▋          | 0.19/0.33333 [00:00<00:00,  1.16s/degrees]
xpos:  84%|████████████████████▏   | 0.28/0.33333 [00:00<00:00,  1.15s/degrees]
xpos:  98%|██████████████████████▌| 0.327/0.33333 [00:00<00:00,  1.29s/degrees]
xpos: 100%|██████████████████████▉| 0.333/0.33333 [00:00<00:00,  1.57s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         5 | 14:02:52.1 |    0.33300 |    6000000 |          3 |          2 |          3 |          3 |          0 |          0 |
xpos:  27%|██████▎                | 0.091/0.33367 [00:00<00:00,  1.26s/degrees]
xpos:  57%|█████████████▏         | 0.191/0.33367 [00:00<00:00,  1.13s/degrees]
xpos:  84%|███████████████████▎   | 0.281/0.33367 [00:00<00:00,  1.13s/degrees]
xpos:  98%|██████████████████████▌| 0.327/0.33367 [00:00<00:00,  1.27s/degrees]
xpos: 100%|█████████████████████| 0.33367/0.33367 [00:00<00:00,  1.55s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         6 | 14:02:54.2 |    0.66700 |    6000000 |          3 |          3 |          3 |          3 |          0 |          0 |
xpos:  27%|███████                   | 0.09/0.333 [00:00<00:00,  1.90s/degrees]
xpos:  57%|██████████████▊           | 0.19/0.333 [00:00<00:00,  1.43s/degrees]
xpos:  84%|█████████████████████▊    | 0.28/0.333 [00:00<00:00,  1.33s/degrees]
xpos:  98%|████████████████████████▌| 0.327/0.333 [00:00<00:00,  1.45s/degrees]
xpos: 100%|█████████████████████████| 0.333/0.333 [00:00<00:00,  1.72s/degrees]
xpos [In progress. No progress bar available.]

vsc16 [In progress. No progress bar available.]
vsc16 [In progress. No progress bar available.]

|         7 | 14:02:56.3 |    1.00000 |    6000000 |          3 |          4 |          3 |          2 |          0 |          0 |
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
generator scan ['fb41419c'] (scan num: 1)
/home/apsu/Apps/miniconda3/envs/bluesky_2021_1/lib/python3.8/site-packages/bluesky/callbacks/fitting.py:165: RuntimeWarning: invalid value encountered in double_scalars
  np.sum(input * grids[dir].astype(float), labels, index) / normalizer



[32]:
('fb41419c-0aef-4ff4-8992-7aa2b62f9931',)
../_images/howto__basic_motor_scaler_scan_62_4.png

The scan ran, data was collected and printed at each step of the scan. Finally, plots were made of the scaler channel vs motor position for each active channel.

Fix a few problems#

There were some problems. First is that an error was reported after the scan (... callbacks/fitting.py:165: RuntimeWarning: invalid value encountered in double_scalars). This error is because the scalers showed no peak during the scan. The scaler is a simulator with no real data. We’ll ignore that error here.

Another problem is that all named scaler channels are shown. Let’s reduce that list to I0 and scint just to show how it is done. (Set back to default: vsc16.select_channels())

[33]:
vsc16.select_channels(['I0', 'scint'])

Another problem is that the scaler counts for 0.1s longer than we have configured. This is a problem with the underlying EPICS support for a simulated (a.k.a. soft channel) scaler. We’ll ignore that error here.

Next problem is that the progress bar is a nuisance in this notebook so we’ll remove it.

[34]:
RE.waiting_hook = None

Then, repeat the same scan.

[35]:
RE(bp.scan([vsc16], xpos, -1, 1, 7))


Transient Scan ID: 2     Time: 2021-08-19 14:02:58
Persistent Unique Scan ID: 'b5f76f7c-78f2-45a5-8264-e0af2ba540bf'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |       xpos |         I0 |      scint |
+-----------+------------+------------+------------+------------+
|         1 | 14:03:01.6 |   -1.00000 |          2 |          2 |
|         2 | 14:03:03.0 |   -0.66700 |          3 |          3 |
|         3 | 14:03:04.4 |   -0.33300 |          2 |          2 |
|         4 | 14:03:05.8 |    0.00000 |          1 |          1 |
|         5 | 14:03:07.2 |    0.33300 |          2 |          2 |
|         6 | 14:03:08.5 |    0.66700 |          1 |          1 |
|         7 | 14:03:09.9 |    1.00000 |          4 |          1 |
+-----------+------------+------------+------------+------------+
generator scan ['b5f76f7c'] (scan num: 2)



[35]:
('b5f76f7c-78f2-45a5-8264-e0af2ba540bf',)
../_images/howto__basic_motor_scaler_scan_68_2.png

Scan with a different counting time : staging#

Suppose that we wish to use a different counting time, we could change the vsc16.preset_time value before running the scan. Another way is to use the ophyd concept of stage & unstage.

Staging is the action of preparing an ophyd device for operation, then resetting it afterwards to its previous values. For our scaler, we could stage a different counting time that would be used during the run, then removed after the run is complete. The stage() and unstage() methods are controlled by an OrderedDictionary where the keys are the attributes of the Python object and the values are used during the run. The RE() takes care of calling stage() and unstage() during the scan.

Here we show staging of a 2.0s preset_time for the run. Also shown are the preset_time value before and after the run.

[36]:
print(f"{vsc16.preset_time.get() = }")
vsc16.preset_time.get() = 0.5
[37]:
vsc16.stage_sigs["preset_time"] = 2.0

Repeat the same scan.

[38]:
RE(bp.scan([vsc16], xpos, -1, 1, 7))


Transient Scan ID: 3     Time: 2021-08-19 14:03:10
Persistent Unique Scan ID: '04dd2069-cca5-4b55-9ff2-7d4d474cb2e6'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |       xpos |         I0 |      scint |
+-----------+------------+------------+------------+------------+
|         1 | 14:03:15.2 |   -1.00000 |         10 |         12 |
|         2 | 14:03:18.2 |   -0.66700 |          9 |          8 |
|         3 | 14:03:21.1 |   -0.33300 |         10 |          9 |
|         4 | 14:03:24.0 |    0.00000 |         11 |         12 |
|         5 | 14:03:26.9 |    0.33300 |         11 |          9 |
|         6 | 14:03:29.8 |    0.66700 |          8 |         11 |
|         7 | 14:03:32.7 |    1.00000 |         11 |         12 |
+-----------+------------+------------+------------+------------+
generator scan ['04dd2069'] (scan num: 3)



[38]:
('04dd2069-cca5-4b55-9ff2-7d4d474cb2e6',)
../_images/howto__basic_motor_scaler_scan_73_2.png

Custom plan with configurable count time#

It is common to want to set the count time at the time the scan is started. For this feature, a custom scan plan is needed, where we will repeat the steps just shown. This plan will use similar arguments as the bp.scan() used above, but add an optional keyword argument ct for the count time with a default of 1.0 second. For housekeeping, we’ll remove the staging configuration from the scaler after the scan.

A bluesky plan is a Python generator function. The bp.scan() call is the part that makes this a generator function. Briefly, a bluesky plan defers execution of the actual scan until the RE() calls for it. This indirection allows the RE() to manage the plan’s execution when the beam dumps or other interruptions occur. (A function becomes a generator by the presence of one or more yield commands.)

[39]:
def tscan(scaler, pos, pStart, pEnd, nPts, ct=1):
    scaler.stage_sigs["preset_time"] = ct
    print(f"{scaler.preset_time.get() = }")

    yield from bp.scan([scaler], pos, pStart, pEnd, nPts)

    print(f"{scaler.preset_time.get() = }")
    del scaler.stage_sigs["preset_time"]

Run the custom plan with 2 seconds count time per point (so we can see this in the time axis).

[40]:
RE(tscan(vsc16, xpos, -1, 1, 7, 2))
scaler.preset_time.get() = 0.5


Transient Scan ID: 4     Time: 2021-08-19 14:03:33
Persistent Unique Scan ID: '6a8d542f-46b4-4fa4-a322-a9a81f2d7de9'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |       xpos |         I0 |      scint |
+-----------+------------+------------+------------+------------+
|         1 | 14:03:37.9 |   -1.00000 |          9 |         10 |
|         2 | 14:03:40.9 |   -0.66700 |         10 |         10 |
|         3 | 14:03:43.8 |   -0.33300 |          9 |          8 |
|         4 | 14:03:46.7 |    0.00000 |         12 |         12 |
|         5 | 14:03:49.6 |    0.33300 |          9 |          8 |
|         6 | 14:03:52.5 |    0.66700 |         12 |         11 |
|         7 | 14:03:55.4 |    1.00000 |         11 |          9 |
+-----------+------------+------------+------------+------------+
generator scan ['6a8d542f'] (scan num: 4)



scaler.preset_time.get() = 0.5
[40]:
('6a8d542f-46b4-4fa4-a322-a9a81f2d7de9',)
../_images/howto__basic_motor_scaler_scan_77_2.png

Challenges#

Try these additional modifications or activities.

  1. Use logger to report at various places.

    Hint: logger.info("text: %s  value: %g", s1, v2, ...)

    Consider examples as used in instrument/plans/peak_finder_example.py. See the Python logging tutorial for more information.

  2. Write a custom plan that accepts user-provided metadata.

    Hint: bp.scan() has the md keyword argument (kwarg) that accepts a dictionary of key: value pairs as the metadata when the plan is run. Your custom plan should accept the same kwarg and pass this to bp.scan(md=the_dictionary), possibly after adding some of its own keys to the metadata.