Basic Operation in Bluesky

Note

Bluesky provides a thorough tutorial. The content here should be taken as a quick reference or crib sheet.

Commands are grouped into “plans”. All plans in the Bluesky interface must be passed to the RunEngine, usually accessible by the variable RE.

In [1]: RE( <plan> )
<plan output>

The RunEngine then takes care of orchestrating the experiment, sending signals to devices, and storing data.

Standard plans

Standard plans are stored under the variable bp, which stands for bluesky.plans. Most operations should eventually be folded into customized plans, but these will get the job done until then. Detailed documentation on Bluesky’s pre-assembled plans can be found here. Examples here will utilize basic Bluesky plans, which may be too verbose for the average user. More specialized plans will be covered in the Plans section

Scanning detectors: bp.scan

The scan plan takes in a list of detectors to measure, a motor to scan, a start coordinate, stop coordinate, and number of measurements.

In [3]: RE(bp.scan([det], motor, -1, 1, 5))
Transient Scan ID: 1     Time: 2020-08-27 15:57:41
Persistent Unique Scan ID: '2a328312-e6e8-4127-a815-05f7020c6b2a'
New stream: 'primary'
+-----------+------------+------------+------------+
|   seq_num |       time |      motor |        det |
+-----------+------------+------------+------------+
|         1 | 15:57:41.5 |     -1.000 |      0.607 |
|         2 | 15:57:41.5 |     -0.500 |      0.882 |
|         3 | 15:57:41.8 |      0.000 |      1.000 |
|         4 | 15:57:41.8 |      0.500 |      0.882 |
|         5 | 15:57:41.9 |      1.000 |      0.607 |
+-----------+------------+------------+------------+
generator scan ['2a328312'] (scan num: 1)

Out[3]: ('2a328312-e6e8-4127-a815-05f7020c6b2a',)

One can also perform a relative scan with bp.rel_scan

In [4]: %mov motor 1

In [5]: RE(bp.rel_scan([det], motor, -1, 1, 5))


Transient Scan ID: 16     Time: 2020-09-17 18:40:56
Persistent Unique Scan ID: '223510b1-fe9e-4b12-b427-70e5370ed86c'
New stream: 'baseline'
New stream: 'primary'
+-----------+------------+------------+------------+
|   seq_num |       time |      motor |        det |
+-----------+------------+------------+------------+
|         1 | 18:40:56.7 |      0.000 |      1.000 |
|         2 | 18:40:56.7 |      0.500 |      0.882 |
|         3 | 18:40:56.8 |      1.000 |      0.607 |
|         4 | 18:40:56.8 |      1.500 |      0.325 |
|         5 | 18:40:56.8 |      2.000 |      0.135 |
+-----------+------------+------------+------------+
generator rel_scan ['223510b1'] (scan num: 16)
Out[5]: ('223510b1-fe9e-4b12-b427-70e5370ed86c',)

Scanning a grid of points: bp.grid_scan

Relevant to High Throughput operations at 1-5 is the grid scan plan. This plan scans multiple motors, with the option to either raster (default) or snake along axes. Snaking will be more time efficient, but the time saved depends on factors such as motor speed More detailed descriptions of these plans can be found here. Snaking explained

In [31]: RE(bp.grid_scan([det],
    ...:  motor1, -1, 1, 3,
    ...:  motor2, -3, 3, 7, True))


Transient Scan ID: 17     Time: 2020-09-17 18:55:11
Persistent Unique Scan ID: 'd25cc1c3-a4bb-45b4-aabe-83379c8e9e18'
New stream: 'baseline'
New stream: 'primary'
+-----------+------------+------------+------------+------------+
|   seq_num |       time |     motor1 |     motor2 |        det |
+-----------+------------+------------+------------+------------+
|         1 | 18:55:11.1 |     -1.000 |     -3.000 |      0.607 |
|         2 | 18:55:11.2 |     -1.000 |     -2.000 |      0.607 |
|         3 | 18:55:11.2 |     -1.000 |     -1.000 |      0.607 |
|         4 | 18:55:11.3 |     -1.000 |      0.000 |      0.607 |
|         5 | 18:55:11.3 |     -1.000 |      1.000 |      0.607 |
|         6 | 18:55:11.3 |     -1.000 |      2.000 |      0.607 |
|         7 | 18:55:11.6 |     -1.000 |      3.000 |      0.607 |
|         8 | 18:55:11.6 |      0.000 |      3.000 |      0.607 |
|         9 | 18:55:11.6 |      0.000 |      2.000 |      0.607 |
|        10 | 18:55:11.6 |      0.000 |      1.000 |      0.607 |
|        11 | 18:55:11.6 |      0.000 |      0.000 |      0.607 |
|        12 | 18:55:11.6 |      0.000 |     -1.000 |      0.607 |
|        13 | 18:55:11.6 |      0.000 |     -2.000 |      0.607 |
|        14 | 18:55:11.6 |      0.000 |     -3.000 |      0.607 |
|        15 | 18:55:11.6 |      1.000 |     -3.000 |      0.607 |
|        16 | 18:55:11.7 |      1.000 |     -2.000 |      0.607 |
|        17 | 18:55:11.7 |      1.000 |     -1.000 |      0.607 |
|        18 | 18:55:11.7 |      1.000 |      0.000 |      0.607 |
|        19 | 18:55:11.7 |      1.000 |      1.000 |      0.607 |
|        20 | 18:55:11.7 |      1.000 |      2.000 |      0.607 |
|        21 | 18:55:11.7 |      1.000 |      3.000 |      0.607 |
+-----------+------------+------------+------------+------------+
generator grid_scan ['d25cc1c3'] (scan num: 17)
Out[31]: ('d25cc1c3-a4bb-45b4-aabe-83379c8e9e18',)

Grid scans: bp.grid_scan

Scanning over an array of samples arranged in a grid is handled by the ‘grid scan’ plan. A more complete discussion can be found on the official documentation

Move motors: bps.mv

The one exception to the rule is the “move” plan, which is a “plan_stub” used as a component of other plans. As such it is accessed via bps.mv.

In [1]: RE(bps.mv(motor, 2))
Out[1]: ()

Can also access via “ipython magic” shortcuts

In [2]: %mv motor 2

Convenience Functions

Motor position summary: %wa

In [1]: %wa
motors
    Positioner                     Value       Low Limit   High Limit  Offset
    motor                          0           AttributeError AttributeError AttributeError
    motor1                         0           AttributeError AttributeError AttributeError
    motor2                         0           AttributeError AttributeError AttributeError

    Local variable name                    Ophyd name (to be recorded as metadata)
    motor                                  motor
    motor1                                 motor1
    motor2                                 motor2

Accessing saved data

You can access saved data through the databroker instance, which should be accessible as db in the bluesky sesison. Some standard patterns are demonstrated below: Official Documentation

In [19]: uid = RE(bp.scan([det], motor, -1, 1, 5))


Transient Scan ID: 15     Time: 2020-09-17 18:13:03
Persistent Unique Scan ID: 'e2d02570-9b8f-4254-b205-107ba990d740'
New stream: 'baseline'
New stream: 'primary'
+-----------+------------+------------+------------+
|   seq_num |       time |      motor |        det |
+-----------+------------+------------+------------+
|         1 | 18:13:03.4 |     -1.000 |      0.607 |
|         2 | 18:13:03.5 |     -0.500 |      0.882 |
|         3 | 18:13:03.5 |      0.000 |      1.000 |
|         4 | 18:13:03.5 |      0.500 |      0.882 |
|         5 | 18:13:03.5 |      1.000 |      0.607 |
+-----------+------------+------------+------------+
generator scan ['e2d02570'] (scan num: 15)

In [20]: db[-1].table()  # -1 designates the last run.  -2 would be second most recent, and so on.
Out[20]:
                                time       det  motor  motor_setpoint
seq_num
1       2020-09-18 01:13:03.437488556  0.606531   -1.0            -1.0
2       2020-09-18 01:13:03.519411087  0.882497   -0.5            -0.5
3       2020-09-18 01:13:03.561402082  1.000000    0.0             0.0
4       2020-09-18 01:13:03.569704294  0.882497    0.5             0.5
5       2020-09-18 01:13:03.576864004  0.606531    1.0             1.0

In [24]: db[-1].table('baseline')  # we have set up bluesky to take baseline measurements before and after each run
Out[24]:
                                time  s_stage_px  ...  s_stage_th  s_stage_th_user_setpoint
seq_num                                            ...
1       2020-09-18 01:13:03.422709942        12.0  ...         1.0                       1.0
2       2020-09-18 01:13:03.589560270        12.0  ...         1.0                       1.0

[2 rows x 13 columns]

For those familiar with python data analysis, the db[-1].table() returns a familiar pandas.DataFrame object.

Searching the DataBroker:

One of the key advantages of the Bluesky data collection system is the ability to search data quickly via saved metadata. Again for a more thorough discussion, look here.

To search DataBroker based on metadata, use parentheses:

# Search by plan name.
headers = db(plan_name='scan')

# Search for runs involving a motor with the name 'eta'.
headers = db(motor='eta')

# Search for runs operated by a given user---assuming this metadata was
# recorded in the first place!
headers = db(operator='Dan')

# Search by time range. (These keywords have a special meaning.)
headers = db(since='2015-03-05', until='2015-03-10')

To search DataBroker based on relative indexing or unique id, you can use square brackets:

# Get the most recent run.
header = db[-1]

# Get the fifth most recent run.
header = db[-5]

# Get a list of all five most recent runs, using Python slicing syntax.
headers = db[-5:]

# Get a run whose unique ID ("RunStart uid") begins with 'x39do5'.
header = db['x39do5']

# Get a run whose integer scan_id is 42. Note that this might not be
# unique. In the event of duplicates, the most recent match is returned.
header = db[42]

Managing Metadata

Metadata is stored with each run, and can be set either for a single run or for all future runs. For a more thorough discussion, look here for the full documentation.

Per-run metadata

Information such as sample name or operational note can be added only to the current plan. One-time metadata can be added directly to the RE() call, and will be propogated to all runs in the plan:

In [1]: RE(plan(), sample_id='A', purpose='calibration')

Persistent metadata

For information pertaining to all runs in a beamtime, metadata can be recorded persistently.

To see the current metadata dictionary:

In [8]: show_md()
Persistent Metadata --------------------
beamline_id: SSRL 1-5 HiTp
scan_id: 6
login_id: b_spec@bluedevlx.slac.stanford.edu
pid: 28034
versions: {'bluesky': '1.6.6', 'ophyd': '1.5.3', 'databroker': '1.1.0', 'ssrltools': '0.1', 'epics': '3.4.2', 'numpy': '1.19.1', 'matplotlib': '3.3.1', 'pymongo': '3.11.0'}
proposal_id: testing
----------------------------------------

Setting items in this metadata dictionary follows standard python dictionary syntax. This metadata will be retained across ipython session restarts:

In [10]: RE.md['operator']='roberttk'

In [11]: show_md()
Persistent Metadata --------------------
beamline_id: SSRL 1-5 HiTp
scan_id: 6
login_id: b_spec@bluedevlx.slac.stanford.edu
pid: 28034
operator: roberttk
versions: {'bluesky': '1.6.6', 'ophyd': '1.5.3', 'databroker': '1.1.0', 'ssrltools': '0.1', 'epics': '3.4.2', 'numpy': '1.19.1', 'matplotlib': '3.3.1', 'pymongo': '3.11.0'}
proposal_id: testing
----------------------------------------

Viewing metadata

Most of the useful metadata can be accessed from the ‘run_start’ document:

In [4]: header = db[-1] # grab header for the last/most recent run

In [5]: header.start
Out[5]:
{'uid': '882fe013-33c4-4fc8-b345-48a4eacb8c87',
'time': 1600276751.726932,
'versions': {'ophyd': '1.5.3', 'bluesky': '1.6.6'},
'scan_id': 1,
'plan_type': 'generator',
'plan_name': 'scan',
'detectors': ['det'],
'motors': ['motor'],
'num_points': 3,
'num_intervals': 2,
'plan_args': {'detectors': ["SynGauss(prefix='', name='det', read_attrs=['val'], configuration_attrs=['Imax', 'center', 'sigma', 'noise', 'noise_multiplier'])"],
'num': 3,
'args': ["SynAxis(prefix='', name='motor', read_attrs=['readback', 'setpoint'], configuration_attrs=['velocity', 'acceleration'])",
-1,
1],
'per_step': 'None'},
'hints': {'dimensions': [[['motor'], 'primary']]},
'plan_pattern': 'inner_product',
'plan_pattern_module': 'bluesky.plan_patterns',
'plan_pattern_args': {'num': 3,
'args': ["SynAxis(prefix='', name='motor', read_attrs=['readback', 'setpoint'], configuration_attrs=['velocity', 'acceleration'])",
-1,
1]}}