Lifecycle of a Plan#

The following demonstrates exactly what the code does with a plan through its lifecycle of being written, loaded and run. Take the following plan.

import bluesky.plans as bp

from typing import Any, List, Mapping, Optional, Union

from bluesky.protocols import Readable
from bluesky.utils import MsgGenerator
from dodal.beamlines import my_beamline

def count(
    detectors: List[Readable] = [my_beamline.det(connect_immediately=False)],
    num: int = 1,
    delay: Optional[Union[float, List[float]]] = None,
    metadata: Optional[dict[str, Any]] = None,
) -> MsgGenerator:
    """
    Take `n` readings from a collection of detectors

    Args:
        detectors (List[Readable]): Readable devices to read: when being run in Blueapi
                                    defaults to fetching a device named "det" from its
                                    context, else will require to be overridden.
        num (int, optional): Number of readings to take. Defaults to 1.
        delay (Optional[Union[float, List[float]]], optional): Delay between readings.
                                                            Defaults to None.
        metadata (Optional[dict[str, Any]], optional): Key-value metadata to include
                                                        in exported data.
                                                        Defaults to None.

    Returns:
        MsgGenerator: _description_

    Yields:
        Iterator[MsgGenerator]: _description_
    """

    yield from bp.count(detectors, num, delay=delay, md=metadata)

Loading and Registration#

Blueapi will load this plan into its context if configured to load either this module or a module that imports it. The BlueskyContext will go through all global variables in the module and register them if it detects that they are plans.

At the point of registration it will inspect the plan’s parameters and their type hints, from which it will build a pydantic model of the parameters to validate against. In other words, it will build something like this:

from pydantic import BaseModel
from dodal.beamlines import my_beamline

class CountParameters(BaseModel):
    detectors: List[Readable] = [my_beamline.det(connect_immediately=False)]
    num: int = 1
    delay: Optional[Union[float, List[float]]] = None
    metadata: Optional[dict[str, Any]] = None

    class Config:
        arbitrary_types_allowed = True
        validate_all = True

This is for illustrative purposes only, this code is not actually generated, but an object resembling this class is constructed in memory. The default arguments will be validated by the context when the plan is run. my_beamline.det(connect_immediately=False) evaluates to a lazily created singleton device. The model is also stored in the context.

Startup#

On startup, the context is passed to the worker, which is passed to the service. The worker also holds a reference to the RunEngine that can run the plan.

Request#

A user can send a request to run the plan to the service, which includes values for the parameters. It takes the form of JSON and may look something like this:

{
    "name": "count",
    "params": {
        "detectors": [
            "andor",
            "pilatus"
        ],
        "num": 3,
        "delay": 0.1
    }
}

The Service receives the request and passes it to the worker, which holds it in an internal queue and executes it as soon as it can.

Validation#

See also

Type Validators for an in-depth explanation of how blueapi knows when to resolve strings as device names

The pydantic model from earlier, as well as the plan function itself, is loaded out of the registry. The parameter values in the request are validated against the model, this includes looking up devices with names andor and pilatus or, if detectors was not passed det.

Execution#

The validated parameter values are then passed to the plan function, which is passed to the RunEngine. The plan is executed. While it is running, the Worker will publish

  • Changes to the state of the RunEngine

  • Changes to any device statuses running within the plan (e.g. when a motor changes position)

  • Event model documents emitted by the RunEngine

  • When the plan starts, finishes or fails.

If an error occurs during any of the stages from “Request” onwards it is sent back to the user over the message bus.