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.

from collections.abc import Iterable
from typing import Any

import bluesky.plans as bp
from bluesky.protocols import Readable
from bluesky.utils import MsgGenerator
from dodal.common.coordination import inject

_DEFAULT_DETECTORS = [inject("my_detector")]


def count(
    detectors: list[Readable] = _DEFAULT_DETECTORS,
    num: int = 1,
    delay: float | Iterable[float] = 0.0,
    metadata: dict[str, Any] | None = 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 collections.abc import Iterable
from typing import Any

from bluesky.protocols import Readable
from dodal.common.coordination import inject
from pydantic import BaseModel, ConfigDict

_DEFAULT_DETECTORS = [inject("my_detector")]


class CountParameters(BaseModel):
    detectors: list[Readable] = _DEFAULT_DETECTORS
    num: int = 1
    delay: float | Iterable[float] = 0.0
    metadata: dict[str, Any] | None = None

    model_config = ConfigDict(
        extra="forbid",
        arbitrary_types_allowed=True,
        validate_default=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 passes it to the worker, which holds it in an internal queue
and executes it as soon as it can. 


## Validation

:::{seealso}
[Type Validators](./type_validators.md) 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.