Skip to content

How presenters run plans

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

How to run a plan from a presenter and How to write a plan that runs until stopped show the code this page describes.


Plans that end by themselves

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.


Running a session's plans

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.
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 threaddocumentcallbacks run walkthe planand its callbacks a Future,at oncedocumentsFuture doneplan ended
viewpresenterRunEngineon its own threaddocumentcallbacks run walkthe planand its callbacks a Future,at oncedocumentsFuture doneplan ended

The callbacks a run gets are the ones its PlanEntry lists, then those the user attached to it.


From a plan to its widget

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.

Plans that are refused

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 plans

@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.

Actions while a plan runs

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 threadActionManager toggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManager toggle 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 threadActionManager toggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManager toggle 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 threadActionManager toggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManager toggle 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 threadActionManager toggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManager toggle 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 threadActionManager toggle on: launchwait(SNAP)snap offeredrequest('snap')wait returns 'snap'snap runningrecord a framedone('snap'), then wait againtoggle off: stop
viewplanon the engine's threadActionManager toggle 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.

Toggle actions

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.

Following an action from a view

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 it the view disables the buttonand shows a toggle releasedoffered: a plan waits for it the view enables the buttonrunning: the plan took itand has not finished it the view disables a clickedbutton; a toggle staysenabled, to be released waitasked foranother was taken,or the plan stoppeddone
idle: no plan waits for itand none runs it the view disables the buttonand shows a toggle releasedoffered: a plan waits for it the view enables the buttonrunning: the plan took itand has not finished it the view disables a clickedbutton; a toggle staysenabled, to be released waitasked 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.


See also