How to add a new plan#
This page covers writing and registering a custom Bluesky plan or
plan stub for the id3c instrument.
For the difference between a plan and a plan stub, see Plans and stubs.
Where things live#
src/id3c/plans/– plan modules. One file per topic; group related plans in one file.src/id3c/startup.py– imports plans at session end so they become available at the IPython prompt.
Skeleton#
# src/id3c/plans/my_plans.py
"""Plans for ___ at 3-ID-C."""
import logging
from bluesky import plan_stubs as bps
from bluesky import plans as bp
from bluesky.utils import plan
logger = logging.getLogger(__name__)
@plan
def park_and_count(detectors, num=1, md=None):
"""Park the sample stage, then count.
Parameters
----------
detectors : list
Detectors to read in the count.
num : int
Number of counts.
md : dict, optional
Extra metadata to attach to the run.
"""
md = md or {}
md = {"purpose": "park-then-count", **md}
yield from bps.mv(sample_stage.xprime, 0, sample_stage.base_y, 0)
yield from bp.count(detectors, num=num, md=md)
Then register it in startup.py so the prompt sees it:
# src/id3c/startup.py (at the bottom, with the other plan imports)
from .plans.my_plans import park_and_count # noqa: E402, F401
Restart the IPython session; park_and_count is now available:
RE(park_and_count([scaler], num=5))
Repository conventions#
These are the established conventions from AGENTS.md; follow them in new plans.
Decorate with @plan#
Always:
from bluesky.utils import plan
@plan
def my_plan(...):
...
@plan wraps the generator so that discarding it without iteration
(the common new-user mistake of my_plan() instead of
RE(my_plan())) emits a RuntimeWarning pointing at the user’s call
site. See The @plan
decorator.
Examples in docstrings use RE(...)#
Any code example in a plan’s docstring should use
RE(my_plan(...)). This is the pattern users see in the rest of
the docs and the help() output should match. Direct
yield from my_plan(...) examples are appropriate only when
showing composition of one plan inside another.
Plan stubs vs. plans#
If your function publishes documents (uses bp.* plans, or
explicit bps.open_run / bps.close_run), it is a plan.
If it does not (only bps.mv, bps.sleep, etc.), it is a
plan stub.
The decorator is the same; the difference is what your function
should be composed into. Plans are typically run by the user
at the top level (RE(my_plan())). Plan stubs are typically
called from inside another plan
(yield from my_stub(...)), though both work either way.
Metadata#
Accept a md kwarg in plans that publish documents, merge it with
your defaults, and pass it through:
@plan
def my_plan(detectors, md=None):
md = md or {}
md = {"plan_name": "my_plan", **md}
yield from bp.count(detectors, md=md)
This lets users attach their own metadata without your plan silently discarding it.
Composing plan stubs#
Inside a plan you are writing, use yield from to call another
plan or stub:
@plan
def align_then_scan(detectors):
yield from align_sample() # another plan you wrote
yield from bps.sleep(0.5)
yield from bp.scan(detectors, sample_stage.xprime, 0, 10, 11)
You do not use RE(...) inside another plan. RE(...) is the
top-level invocation only.
If you forget yield from, the inner generator is created and
discarded – the call does nothing. This is the same bug
@plan is
designed to catch; decorating both the outer and inner plan turns
the silent no-op into a visible warning.
Testing without EPICS#
This repo is developed on a host that cannot reach the beamline EPICS PVs. For an offline check that a plan is at least shaped right:
from id3c.plans.my_plans import park_and_count
# 1. Decorator marker present?
park_and_count._is_plan_ # True if @plan applied
# 2. Calling it returns a Plan, not None?
park_and_count([], num=1) # <Plan object ...>
# 3. Smoke-iterate to surface obvious errors (will raise on the first
# real device access, which is expected off-network):
gen = park_and_count([], num=1)
next(gen) # the first yielded message
For full validation, run the plan on a workstation that can reach the IOCs.
See also#
Plans and stubs – the conceptual background.
The RunEngine – why
yield fromworks andRE(...)exists.How to run a scan – the built-in plans your custom plans usually compose.