A plan is a recipe for an acquisition: a Python generator
that says, one step at a time, what to move, what to read and when. Plans come
from bluesky, and most of what its
documentation on plans
says applies here too.
In redsun, any component can offer plans, and one
presenter runs them on a
RunEngine, the object that executes plans. redsun
adds three things to bluesky:
a RunEngine that runs the plan on a thread of its own and returns at once,
where the bluesky one blocks the caller and freezes a window
PlanSpec, a description of a plan's parameters read from its signature,
from which a view in any toolkit builds its controls,
for Qt a plan widget
continuous plans, which run until stopped and take actions from the user
meanwhile, such as recording a frame on request
The simplest plan does its steps and stops. It's an ordinary bluesky plan
whose parameters are annotated by protocols rather
than classes, as in
Describing a device with a protocol, so it
works with any device that has what it reads and sets.
A component offers its plans through a plan_map method returning each under
its name as a PlanEntry, which makes it a
HasPlans. An entry can list the
document callbacks the plan needs and say, with
extendable, whether the user may attach more. Plans stay with the component
they belong to; no central list names them.
The presenter asks the session for every HasPlans, so a session file that
adds such a component adds its plans too, with no change to the presenter
(Questions). Step through a plan reaching its widget:
From a plan to its widget
A component offers its plans through plan_map, which returns each one under its name as a PlanEntry.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.
In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.
create_plan_spec reads each plan's signature and type hints into a PlanSpec, a description of every parameter.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.PlanSpecone per plancreate_plan_spec reads the plan's signature and type hints into a description of each parameter.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.create_plan_spec reads the plan's signature and type hints into a description of each parameter.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.PlanSpecone per plancreate_plan_spec reads the plan's signature and type hints into a description of each parameter.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.create_plan_spec reads the plan's signature and type hints into a description of each parameter.
A view builds a plan widget from each PlanSpec: one control per parameter, and a list of devices for each device parameter.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.plan widgetin a viewThe view builds one control per parameter, and a list of devices for each device parameter.PlanSpecone per plancreate_plan_spec reads the plan's signature and type hints into a description of each parameter.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.The view builds one control per parameter, and a list of devices for each device parameter.create_plan_spec reads the plan's signature and type hints into a description of each parameter.
componentplan_map()Any component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.presenterholds the RunEngineIn setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.plan widgetin a viewThe view builds one control per parameter, and a list of devices for each device parameter.PlanSpecone per plancreate_plan_spec reads the plan's signature and type hints into a description of each parameter.planscreate_plan_speccontrolsAny component with a plan_map method satisfies HasPlans. It returns each plan under its name, as a PlanEntry.In setup, the presenter asks the session for every component that satisfies HasPlans, and keeps the plans they offer.The view builds one control per parameter, and a list of devices for each device parameter.create_plan_spec reads the plan's signature and type hints into a description of each parameter.
The view in How to run a plan from a presenter
asks the same question and describes each plan itself, with the session's
devices to list what can fill each device parameter. The built-in
AcquisitionView instead takes the
presenter in setup, as a DescribesPlans,
and reads its descriptions. They can't be a
shared value: the session reads those when it
builds a component, and the presenter describes its plans later, in setup.
Calling the engine returns a Future at once; the presenter uses it to tell
the view when the plan ended:
One run of a plan
viewpresenterRunEngineon its own threaddocumentcallbacksrun walkthe planand its callbacksa Future,at oncedocumentsFuture doneplan ended
viewpresenterRunEngineon its own threaddocumentcallbacksrun walkthe planand its callbacksa Future,at oncedocumentsFuture doneplan ended
The callbacks a run gets are the ones its PlanEntry lists, then those the
user attached to it.
create_plan_spec makes a
PlanSpec from the plan's signature in plain Python, with no toolkit, so you
can inspect a plan before any application object exists.
A plan is never shown with a control nobody can fill in:
How create_plan_spec treats a parameter
create_plan_spec asks two questions of every parameter. Step through three examples.
a parameterof the plancan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
a parameterof the plancan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
steps: int = 5 gets a control, since a control can show an int.
steps: int = 5can a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
steps: int = 5can a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
md: dict[str, Any] | None = None can't be shown, but it has a default, so it is hidden and the plan keeps the default.
md: dict[str, Any]| None = Nonecan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
md: dict[str, Any]| None = Nonecan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
value: Any can't be shown and has no default, so create_plan_spec raises UnresolvableAnnotationError.
value: Anycan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
value: Anycan a controlshow its type?a control inthe plan widgetdoes it havea default?hidden: the plankeeps its defaultThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.UnresolvableAnnotationErrorAny is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.yesnoyesnoThe view leaves the parameter out. This is how the md parameter of most bluesky plans is treated.Any is refused on purpose, and so is a missing annotation, which reads as Any. A control for it would accept everything and show up as a bare text field.
It also raises ValueError for two actions with the same name, or a default
that breaks its own limits. A component that catches the error per plan
leaves that plan out and keeps the others, as
AcquisitionPresenter does with a
warning; one that doesn't fails its whole setup. The
reference covers how each
annotation is read and how a widget's values become a call.
@continuous marks a plan that runs until stopped; create_plan_spec reads
it into PlanSpec.continuous and PlanSpec.pausable, from which the view
builds a start/stop toggle and, with pausable=True, a pause button. Stopping
is the normal end, so the RunEngine closes the stopped plan's
run with exit status success.
An action is something the user triggers while the plan runs. PlanAction
declares it (name, description, button labels; a frozen dataclass with no
state), and an ActionManager, owned by the component offering the plans,
keeps each action's state for the plans to wait on. A plan names an action as
a parameter default, such as snap: PlanAction = SNAP, and create_plan_spec
makes its button. Step through a live view that records a frame on request:
A continuous plan with one action
The view launches the plan. The plan waits for its action, and the ActionManager offers it to the view.
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
The user presses the button: the view sends the request, and the presenter passes it to the ActionManager of the component that offers the plan.
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
wait returns 'snap' to the plan, and the view learns that snap is running.
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
The plan records a frame, calls done('snap'), and waits again.
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
The user toggles the plan off: the presenter stops the RunEngine, which closes the run with the exit status success.
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManagertoggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
wait returns the first action asked for, which runs until the plan calls
done; the others go back to idle, as all do if the plan is stopped while it
waits.
A stopped plan leaves its running action running
An action that is running when the plan is stopped stays running until
the plan calls done. Call done in a finally block.
Each wait makes new latches, objects a plan waits on until another thread
sets them (engine reference),
so a request left over from an earlier launch can't start this one's action;
the RunEngine has no code for actions and only waits on those latches.
wait doesn't time out: it yields a checkpoint
every poll_interval seconds and does nothing else, so a plan that shows
frames while it waits needs a device that streams on its own.
The user asks through request, a slot safe from any
thread that raises nothing; asking for an action no plan offers, or ending one
that isn't running, only logs a warning.
An action with toggle_states gets a button that stays pressed, showing the
first label released and the second pressed; with the default None it's
clicked. Releasing it sends request(name, on=False), which the plan waits
for with wait_released.
ActionManager.sig_changed reports each action's new ActionState, and a
connected view sets its buttons from it. An action is always in one of three
states:
The states of an action
idle: no plan waits for itand none runs itthe view disables the buttonand shows a toggle releasedoffered: a plan waits for itthe view enables the buttonrunning: the plan took itand has not finished itthe view disables a clickedbutton; a toggle staysenabled, to be releasedwaitasked foranother was taken,or the plan stoppeddone
idle: no plan waits for itand none runs itthe view disables the buttonand shows a toggle releasedoffered: a plan waits for itthe view enables the buttonrunning: the plan took itand has not finished itthe view disables a clickedbutton; a toggle staysenabled, to be releasedwaitasked foranother was taken,or the plan stoppeddone
Only the plan changes a state, so the changes arrive in the order they
happened. A request changes no state: the view that made it learns what came
of it from the next state.