Write Bluesky Plans for Blueapi#
See also
The bluesky documentation for an introduction to bluesky plans and general forms/advice. Blueapi has some additional requirements, which are explained below.
Format#
Plans in Python files look like this:
from bluesky.protocols import Readable, Movable
from bluesky.utils import MsgGenerator
from typing import Mapping, Any
def my_plan(
detector: Readable,
motor: Movable,
steps: int,
sample_name: str,
extra_metadata: dict[str, Any]) -> MsgGenerator:
# logic goes here
...
The type annotations (e.g. : str
, : int
, -> MsgGenerator
) are required as blueapi uses them to detect that this function is intended to be a plan and generate its runtime API.
Input annotations should be as broad as possible, the least specific implementation that is sufficient to accomplish the requirements of the plan. For example, if a plan is written to drive a specific motor (MyMotor
), but only uses the general methods on the Movable
protocol, it should take Movable
as a parameter annotation rather than MyMotor
.
Injecting Devices#
Some plans are created for specific sets of devices, or will almost always be used with the same devices, it is useful to be able to specify defaults. Dodal makes this easy with its factory functions.
Injecting Metadata#
The bluesky event model allows for rich structured metadata to be attached to a scan. To enable this to be used consistently, blueapi encourages a standard form.
Plans (as opposed to stubs) should include metadata
as their final parameter, if they do it must have the type dict[str, Any] | None
, and a default of None. If the plan calls to a stub/plan which takes metadata, the plan must pass down its metadata, which may be a differently named parameter.
def pass_metadata(x: Movable, metadata: dict[str, Any] | None = None) -> MsgGenerator:
yield from bp.count{[x], md=metadata or {}}
Docstrings#
Blueapi exposes the docstrings of plans to clients, along with the parameter types. It is therefore worthwhile to make these detailed and descriptive. This may include units of arguments (e.g. seconds or microseconds), its purpose in the function, the purpose of the plan etc.
def temp_pressure_snapshot(
detectors: List[Readable],
temperature: Movable = sample_temperature(),
pressure: Movable = sample_pressure(),
target_temperature: float = 273.0,
target_pressure: float = 10**5,
metadata: Optional[dict[str, Any]] = None,
) -> MsgGenerator:
"""
Moves devices for pressure and temperature (defaults fetched from the context)
and captures a single frame from a collection of devices
Args:
detectors (List[Readable]): A list of devices to read while the sample is at STP
temperature (Optional[Movable]): A device controlling temperature of the sample,
defaults to fetching a device name "sample_temperature" from the context
pressure (Optional[Movable]): A device controlling pressure on the sample,
defaults to fetching a device name "sample_pressure" from the context
target_pressure (Optional[float]): target temperature in Kelvin. Default 273
target_pressure (Optional[float]): target pressure in Pa. Default 10**5
Returns:
MsgGenerator: Plan
Yields:
Iterator[MsgGenerator]: Bluesky messages
"""
yield from move({temperature: target_temperature, pressure: target_pressure})
yield from count(detectors, 1, metadata or {})