Plans¶
Plan authoring surfaces: the plan registry, parameter introspection, the
Annotated UI vocabulary that drives automatic form generation, user plan
files, and embedded plan UIs.
from lightfall.acquire.plans import PlanRegistry, PlanInfo, ParameterInfo, get_registry
from lightfall.ui.annotations import Unit, Decimals, Range, DeviceFilter
from lightfall.acquire.plan_ui import PlanUI, PlanState, plan_with_ui
How plans get registered¶
Route |
Mechanism |
|---|---|
User plan files |
Drop a |
Programmatic |
|
PlanPlugin |
The |
The default registry does not register raw Bluesky builtins (bp.scan,
bp.grid_scan, …) — their *args signatures cannot generate useful
forms. Instead, typed wrapper plans in
lightfall.acquire.plans.lightfall_plans (e.g. scan_1d, rel_scan_1d)
provide the same functionality with annotated signatures. Raw bp.* plans
remain accessible through the agent’s plan-code tool and the IPython
console.
User plans (~/lightfall/plans/)¶
Each file in ~/lightfall/plans/ defines one plan: a module-level variable
named plan that is a generator function yielding Bluesky messages. The
filename (without .py) becomes the plan name; files starting with _ are
skipped.
# ~/lightfall/plans/my_scan.py
import bluesky.plans as bp
def plan(detectors: list, motor, start: float = -10.0,
stop: float = 10.0, num: int = 21):
"""Scan a motor while reading detectors."""
yield from bp.scan(detectors, motor, start, stop, num)
UserPlanService (singleton, lightfall.acquire.plans.UserPlanService):
Member |
Description |
|---|---|
|
Singleton accessor. |
|
Load every |
|
Load one file; registers (or replaces) the plan. |
|
Create a file from the built-in template and load it. |
|
Unload everything and reload from disk. |
|
Path helpers. |
Signals |
|
The service watches the directory with a QFileSystemWatcher: edits reload
the plan in place (register_or_replace), deletions unregister it. Every
change — including failed loads — is auto-committed to a local git
repository via GitTracker, so plan history is preserved.
PlanRegistry¶
lightfall.acquire.plans.registry.PlanRegistry — the central catalog used
by the Plans panel and the agent. Singleton via
PlanRegistry.get_instance() (or the module-level get_registry()).
Member |
Description |
|---|---|
|
Register a plan; raises |
|
Decorator form; |
|
Replace-if-exists variant (used by hot-reload). |
|
Remove a plan; returns |
|
|
|
All plans, optionally filtered by category. |
|
Sorted category names. |
|
Case-insensitive match against name and description. |
|
Property: registered names. |
PlanInfo and ParameterInfo¶
PlanInfo.from_function(name, func, category) builds plan metadata by
introspection: it captures the signature, takes the first docstring
paragraph as the description, parses per-parameter descriptions from
Google- or NumPy-style docstrings, and extracts >>> examples.
PlanInfo fields: name, func, signature, description, category,
parameters (list of ParameterInfo), examples, display_name
(generated from name via name_to_display_name() when unset), and icon
(a (color, letter) tuple; defaults come from PLAN_CATEGORY_ICONS by
category). Helpers: get_display_name(), get_icon(),
get_required_params(), get_optional_params().
ParameterInfo fields: name, annotation, default, kind,
description, plus derived required (no default) and type_name
(display-friendly type).
Annotated UI vocabulary¶
The parameter editor reads typing.Annotated metadata from plan signatures
to render appropriate inputs. The vocabulary lives in
lightfall.ui.annotations (all are frozen dataclasses):
Annotation |
Effect |
|---|---|
|
Display a unit suffix next to a numeric input (e.g. |
|
Decimal precision of a float spinbox. |
|
Min/max bounds for a numeric input. |
|
Default that overrides the signature default. |
|
Restrict a device-selector parameter; criteria AND together. |
|
OR-combination of |
|
Pre-select devices by name or regex. |
|
qtawesome icon for the device-selector button ( |
from typing import Annotated
from lightfall.ui.annotations import Unit, Range, DeviceFilter
def plan(
detectors: Annotated[list, DeviceFilter(category="detector")],
motor: Annotated[object, DeviceFilter(category="motor")],
start: Annotated[float, Unit("mm")] = -10.0,
stop: Annotated[float, Unit("mm")] = 10.0,
num: Annotated[int, Range(min=1)] = 21,
):
...
This is the same vocabulary the built-in typed wrapper plans use, so user plans annotated this way get identical form generation.
🖼️ Image placeholder — Screenshot: the plan configuration form generated from an annotated signature — numeric spinboxes with unit suffixes and a filtered device-selector button.
Embedded plan UIs (plan_with_ui)¶
Plans can ship a runtime UI widget shown as a tab in the Plans panel while
the plan runs (lightfall.acquire.plan_ui):
PlanUI—QWidgetbase for the widget; build the UI in__init__usingself._layout.PlanState—QObjectbase for shared state between the running plan and its UI. Definesstatus_changed = Signal(str)and thestop_requested/pause_requestedflags; subclasses add their own signals and attributes. The plan module keeps a module-level instance that both the plan function and the widget reference directly.@plan_with_ui(UIClass)— decorator that attaches aPlanUIsubclass to a plan function (stored as the_plan_ui_classattribute). The Plans panel instantiates the UI when the plan is submitted.get_plan_ui_class(plan_func)— returns the attached class orNone.
from lightfall.acquire.plan_ui import PlanUI, plan_with_ui
class MyPlanUI(PlanUI):
...
@plan_with_ui(MyPlanUI)
def plan(detectors):
yield from bps.count(detectors)
Qt signals are thread-safe across threads; plans should reset their state object explicitly at the start of each run.
Class reference¶
Type annotation metadata for procedural UI generation.
Provides dataclasses that can be used with typing.Annotated to add UI hints for plan parameters. These annotations control how the parameter editor renders inputs for plan functions.
Usage:
from typing import Annotated
from lightfall.ui.annotations import Unit, Decimals, Range, DeviceFilter
def scan(
energy: Annotated[float, Unit("eV"), Range(0, 10000)],
num_points: Annotated[int, Range(1, 1000)],
motor: Annotated[Device, DeviceFilter(device_class="EpicsMotor")],
): ...
- class lightfall.ui.annotations.Decimals(places: int)[source]¶
Number of decimal places for float display.
Controls the precision of the spinbox widget for float parameters.
Example:
step_size: Annotated[float, Decimals(4)] = 0.001
- class lightfall.ui.annotations.Default(value: Any)[source]¶
Default value for parameter.
Provides a default value that overrides the function signature default. Useful when the annotation-based default differs from the code default.
Example:
exposure: Annotated[float, Default(1.0), Unit("s")]
-
class lightfall.ui.annotations.DeviceDefault(*names: str, pattern: str | None =
None)[source]¶ Default device selection by name or pattern.
Pre-selects devices in the device selector based on explicit names or a regex pattern match.
- Parameters:¶
Example:
# Pre-select specific detector detector: Annotated[list[Detector], DeviceDefault("PI_MTE3")] # Pre-select all devices matching pattern motors: Annotated[list[Motor], DeviceDefault(pattern="sample_.*")]
-
class lightfall.ui.annotations.DeviceFilter(device_class: str | None =
None, category: str | set[str] | None =None, group: str | None =None, source: str | None =None, name_pattern: str | None =None)[source]¶ Filter criteria for device selection.
All specified criteria use AND logic within a single filter. Use DeviceFilterAny to combine filters with OR logic.
- Parameters:¶
- device_class: str | None =
None¶ Match ophyd class name (e.g., “EpicsMotor”, “AreaDetector”).
- category: str | set[str] | None =
None¶ Match device category (e.g., “motor”, “detector”).
- group: str | None =
None¶ Match device group from tags (e.g., “areadetectors”, “magnets”).
- source: str | None =
None¶ Match device source/connection type (e.g., “epics”, “simulated”).
- name_pattern: str | None =
None¶ Regex pattern for device name matching.
- device_class: str | None =
Example:
motor: Annotated[Device, DeviceFilter(device_class="EpicsMotor")] detector: Annotated[Device, DeviceFilter(category="detector", group="areadetectors")]
- class lightfall.ui.annotations.DeviceFilterAny(*filters: DeviceFilter)[source]¶
Combine multiple DeviceFilter with OR logic.
Allows selecting devices that match ANY of the specified filters, enabling disjoint filter criteria (e.g., “motors OR detectors”).
- Parameters:¶
- *filters: DeviceFilter¶
DeviceFilter instances to combine with OR logic.
Example:
# Select either motors or positioners axis: Annotated[Device, DeviceFilterAny( DeviceFilter(category="motor"), DeviceFilter(category="positioner"), )]
- class lightfall.ui.annotations.DeviceIcon(name: str)[source]¶
QtAwesome icon identifier for the device parameter button.
Specifies which icon to show on the device selector button in the plan configuration UI. If the icon string has no dot prefix,
mdi6.is prepended automatically.Example:
motor: Annotated[Device, DeviceFilter(category="motor"), DeviceIcon("engine")]
-
class lightfall.ui.annotations.Range(min: float | int | None =
None, max: float | int | None =None)[source]¶ Min/max bounds for numeric input.
Sets the allowed range for numeric parameters in the spinbox widget.
- Parameters:¶
Example:
num_points: Annotated[int, Range(1, 1000)] = 10