Getting started

ibex_bluesky_core is a library which bridges the IBEX control system and the bluesky data acquisition framework.

Bluesky is a highly flexible data acquisition system, which has previously been used at large-scale research facilities such as NSLS-II and Diamond, among others.

While the bluesky framework itself is generic enough to cope with many forms of data acquisition, one of the core use cases is “scanning” - that is, measuring how some experimental parameter(s) vary with respect to other parameter(s). Bluesky has extensive mechanisms and helpers for typical scanning workflows.

The most important concepts in bluesky are:

  • Plans tell bluesky what to do next, in the form of Messages

  • Devices encapsulate the details of how some specific device is controlled

  • The RunEngine executes plans (possibly interacting with devices)

  • Callbacks do something with data emitted by the scan

Plans

A plan is an iterable of messages. A very simple plan, which doesn’t do anything, is:

from bluesky.utils import Msg

my_plan = [Msg("null")]

Where Msg("null") is an instruction to bluesky (which in this case, does nothing).

While it’s possible to write bluesky plans as any iterable, in practice plans are usually written using python generators, using python’s yield from syntax to delegate to other plans as necessary:

import bluesky.plan_stubs as bps

def plan():
    yield from bps.null()

Devices

ibex_bluesky_core provides built-in support for a number of ISIS-specific devices. For example, blocks are available as devices:

from ibex_bluesky_core.devices.block import block_r, block_mot

mot = block_mot("mot")  # An IBEX block pointing at a motor
det = block_r(float, "p5")  # A readback block with float datatype

Block objects provide several mechanisms for configuring write behaviour - see ibex_bluesky_core.devices.block.BlockWriteConfig for detailed options.

Likewise, the DAE is available as a bluesky device: see the DAE Documentation for full examples including example configurations.

Setting and reading values

Bluesky provides plan stubs for setting & reading values from bluesky devices: bps.mv() and bps.rd() respectively.

from ibex_bluesky_core.devices.block import BlockMot
import bluesky.plan_stubs as bps

def multiply_motor_pos_by_2(mot: BlockMot):
    current_value = yield from bps.rd(mot)
    yield from bps.mv(mot, current_value * 2.0)

Danger

Notice that we are using bps.rd() and bps.mv() here, rather than g.cget() or g.cset(). Bare genie or inst commands must not be used in bluesky plans - instead, prefer to use the bluesky-native functionality - i.e. plans using yield from.

Carefully review calling external code if you do need to call external code in a plan.

For more details about plan stubs (plan fragments like mv and read), see bluesky plan stubs documentation

Scanning

Having created some simple devices, those devices can be used in standard bluesky plans:

from ophyd_async.plan_stubs import ensure_connected
import bluesky.plans as bp
from ibex_bluesky_core.devices.block import block_r, block_mot

def my_plan(det_block_name: str, mot_block_name: str, start: float, stop: float, num: int):
    mot = block_mot(mot_block_name)
    det = block_r(float, det_block_name)

    # Devices connect up-front - this means that plans are generally "fail-fast", and
    # will detect problems such as typos in block names before the whole plan runs.
    yield from ensure_connected(det, mot, force_reconnect=True)
    
    # Delegate to bluesky's scan plan.
    yield from bp.scan([det], mot, start, stop, num)

For details about plans which are available directly from bluesky - like bp.scan above - see bluesky’s plan documentation.

The RunEngine

The RunEngine is the central “conductor” in bluesky - it is responsible for reading a plan and performing the associated actions on the hardware. To get a run engine instance, use:

from ibex_bluesky_core.run_engine import get_run_engine
RE = get_run_engine()

Tip

In the IBEX GUI, manually getting a runengine is unnecessary - it is done automatically.

Then execute a plan using the RunEngine:

RE(my_plan("det", "mot", 0, 10, 5))

Noth that typing my_plan("det", "mot", 0, 10, 5) does not do anything by itself. That is because my_plan is a python generator - which does nothing until iterated. To actually execute the plan, it must be passed to the RunEngine, which is conventionally called RE.

For more detail about the RunEngine, see:

Callbacks

Callbacks are bluesky’s mechanism for listening to data from a scan. Some examples of common callbacks are:

It is possible to use callbacks manually, when executing a plan:

from bluesky.callbacks import LiveTable

RE(my_plan("det", "mot", 0, 10, 5), LiveTable(["mot", "det"]))

However, to save typing out callbacks repeatedly, user-specified plans can add callbacks via subs_decorator:

from ibex_bluesky_core.devices.block import block_r, block_mot
from ophyd_async.plan_stubs import ensure_connected
from bluesky.preprocessors import subs_decorator
from bluesky.callbacks import LiveTable
import bluesky.plans as bp

def my_plan(det_block_name: str, mot_block_name: str, start: float, stop: float, num: int):
    mot = block_mot(mot_block_name)
    det = block_r(float, det_block_name)

    @subs_decorator([
      LiveTable([mot.name, det.name]),
    ])
    def _inner():
      yield from ensure_connected(det, mot, force_reconnect=True)
      yield from bp.scan([det], mot, start, stop, num)
    yield from _inner()

The above will show a LiveTable by default, any time my_plan is executed. The same mechanism can be used for example to always configure a particular scan with plots and a fit with a specific type.

For more information on callbacks, see bluesky callbacks documentation.

See also

Plans & plan-stubs

Callbacks

Full Examples

External documentation