Skip to content

How to choose the inputs of a plan

A plan widget gives each parameter of a plan an input chosen by the parameter's annotation, and the input starts from the parameter's default. This guide shows which annotation gives which input.

Prerequisites

You need a plan a presenter offers, as in How to run a plan from a presenter.

Annotate each parameter

The plan widget puts device parameters in the Devices group and the others in Parameters. Each tab shows a signature and the inputs it gets.

def snap(
    exposure: float = 0.1, frames: int = 10, label: str = "scan"
) -> MsgGenerator[None]:

Spin boxes for exposure and frames, and a text field for label Spin boxes for exposure and frames, and a text field for label

def bin_frames(
    mode: Literal["fast", "slow"] = "fast", binning: Binning = Binning.TWO
) -> MsgGenerator[None]:

A combo box offering fast and slow, and one offering the binnings A combo box offering fast and slow, and one offering the binnings

def scan(
    detectors: Sequence[Readable[Any]],
    motor: Movable[float],
    exposure: float = 0.1,
    frames: int = 10,
) -> MsgGenerator[None]:

A Devices group with a box per readable device and a combo box for the motor, then a Parameters group with exposure and frames A Devices group with a box per readable device and a combo box for the motor, then a Parameters group with exposure and frames

def walk(positions: list[float] = [0.0, 0.5, 1.0]) -> MsgGenerator[None]:

Three rows of positions, each with a remove button, and a button adding one Three rows of positions, each with a remove button, and a button adding one

def acquire(frames: int | None = None) -> MsgGenerator[None]:

An unticked set box beside a disabled spin box An unticked set box beside a disabled spin box

def amplify(gains: dict[str, float] = {"x": 1.0, "y": 2.0}) -> MsgGenerator[None]:

Two rows, each a key field and a value spin box, and a button adding one Two rows, each a key field and a value spin box, and a button adding one

def pause_between(delay: float | list[float] = 0.0) -> MsgGenerator[None]:

A combo box choosing float or list of float above a spin box A combo box choosing float or list of float above a spin box

Lists, sets, mappings, fixed-length tuples, optional values and unions nest, so a dict[str, list[float]] is a table whose values are lists. Every annotation an input can show, and the shapes it can't, are listed in How an annotation is read.

Limit the values a parameter takes

Most plan parameters only make sense in a range. A camera can't expose for 0 seconds, and a scan needs at least one frame. Without limits, the plan widget accepts any number, and you find a wrong value only when the plan runs, when the plan raises or a device refuses the value partway through.

To avoid that, write the limits in the plan's signature with annotated-types. The plan widget stops each input at its limits, and shows why Run is disabled while a value is outside them. The presenter checks the same limits, so a plan started from code gets the same protection:

def expose(
    frames: Annotated[int, Ge(1)] = 1,
    exposure: Annotated[float, Gt(0), Le(10), MultipleOf(0.001)] = 0.1,
    points: Annotated[list[float], MaxLen(3)] = [0.0, 1.0],
) -> MsgGenerator[None]:
A spin box for frames, a spin box for exposure with three decimals, and two rows of points A spin box for frames, a spin box for exposure with three decimals, and two rows of points
Limit What the input does
Ge, Le, Interval the spin box stops at the bound
Gt, Lt for an int, the spin box stops one past the bound; for a float, at the bound, and the bound itself is a problem
MultipleOf the spin box steps by it and shows as many decimals as it needs
MaxLen, Len a list or a mapping stops adding rows at the maximum
MinLen, a text too long a problem

The limits work inside other types too, so list[Annotated[float, Ge(0)]] limits every item. A float with no limit can take any value, and an int any value a 32-bit integer can hold.

A default that breaks its own limits drops the plan

The session leaves the plan out when it builds, and the log says which limit the default breaks. Pick a default inside the limits.

A magicgui option dict in the annotation still shapes the input, for example {"widget_type": "Slider"}. Where it gives a bound that a limit also gives, the limit wins.

Leave a parameter out

The plan widget leaves out a parameter no input can show, such as md: dict[str, Any] | None = None, and the plan keeps its default. Without a default, create_plan_spec refuses the whole plan with UnresolvableAnnotationError.

See why Run is disabled

While an input holds a value the plan can't take, such as a repeated mapping key, a value outside its limits or no device chosen, Run stays disabled and the first reason shows under the parameters. PlanWidget.problems lists every reason.