Plans, plan stubs, and the @plan decorator#
Bluesky distinguishes between plans and plan stubs. Both are generator functions that yield messages for the RunEngine; the technical difference is what they yield.
This page explains the distinction, when it matters, and how the
@plan decorator helps catch a common mistake.
The technical distinction#
Kind |
What it does |
Examples in |
|---|---|---|
plan |
Publishes Bluesky documents. Yields |
|
plan stub |
Does not publish documents. Yields |
|
Plans bracket a run; the RunEngine assigns a run UID and scan_id,
and every document the plan yields becomes part of that run. After
close_run, the RunEngine considers the run finished, fires
“stop” subscribers (file writers flush, etc.), and the catalog gains
one new entry.
Plan stubs run as part of a plan, but do not start or end a run on
their own. They are the building blocks. bp.scan is internally a
collection of bps.mv (move) and bps.trigger_and_read (acquire)
stubs wrapped in open_run / close_run.
Why the distinction matters#
For using Bluesky, the distinction barely matters. You can pass
either to RE(...):
RE(bp.scan([detector], motor, 0, 10, 11)) # a plan
RE(bps.mv(motor, 5)) # a plan stub
Both work. The difference is what gets saved to the catalog:
The
bp.scanproduces a complete Bluesky run – a catalog entry with metadata, an event stream, and the data.The
bps.mvproduces no catalog entry. The motor moves; nothing is recorded.
For writing Bluesky code, the distinction matters a lot:
Authoring a plan stub is easy: write a generator function that yields stubs. No
open_run/close_runneeded; the caller will provide those.Authoring a plan requires you to call
open_run/create/save/close_run(or use a helper likebluesky.preprocessors.run_decorator). Otherwise the data you acquire never gets attached to a run.
Most user code is plan stubs. Composing them into a plan is usually
a matter of calling an existing bp.* plan, not writing one yourself.
The @plan decorator#
bluesky.utils.plan wraps your function so that, if you call it
without RE(...) (or yield from), Python prints a warning
shortly after you press Enter – usually right next to the next
prompt.
It catches the common mistake of typing my_plan(...) at the
IPython prompt instead of RE(my_plan(...)).
Compare:
# Without @plan
def my_plan():
yield from bps.mv(motor, 5)
my_plan() # silently does nothing; no warning
from bluesky.utils import plan
@plan
def my_plan():
yield from bps.mv(motor, 5)
my_plan()
# RuntimeWarning: plan `my_plan` was never iterated,
# did you mean to use `yield from`?
The warning shows up shortly after the prompt returns – typically mixed in with the next prompt line. The traceback it prints points at your call site, not at internal Bluesky code, so you can see exactly which line you typed.
Convention in this repo#
All plans and plan stubs we author are decorated with @plan. See
the AGENTS.md > @plan decorator on our own
plans
section. Examples in this repo:
LaserOptics.move_in/move_out– plan stubs as device methodssim_count_plan,sim_print_plan,sim_rel_scan_plan– plans
The decorator works for both plans and plan stubs; it does not
distinguish them. It also works on instance methods (self is
passed through normally).
What @plan does not do#
It does not turn a non-generator function into a plan. The decorated function still has to
yieldsomething for the RunEngine to do.It does not validate the message stream.
It does not prevent you from calling the function without
RE(...)– it only warns. A tight loop or a script that exits quickly may finish before the warning is printed, so you may not see it in non-interactive contexts.It does not perform any work at decoration time; the cost is one
Planobject wrapper per call.
See also#
The RunEngine – why
RE(...)exists at all.Run a scan – using plans interactively.
Add a plan – writing your own plans, with the
@plandecorator applied per repo convention.