From 1b206a4b33d3318f3225327092f3f02459dc948f Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 27 Apr 2026 18:28:44 +0200 Subject: [PATCH 1/6] feat: add scan documentation --- docs/learn/scans/fast-axis-slow-axis.md | 131 +++++++++++ docs/learn/scans/introduction.md | 128 +++++++++++ docs/learn/scans/learn-by-example.md | 218 +++++++++++++++++++ docs/learn/scans/position-generators.md | 181 ++++++++++++++++ docs/learn/scans/scan-actions.md | 216 +++++++++++++++++++ docs/learn/scans/scan-components.md | 165 ++++++++++++++ docs/learn/scans/scan-definition-info.md | 129 +++++++++++ docs/learn/scans/scan-info.md | 264 +++++++++++++++++++++++ docs/learn/scans/scanargument.md | 159 ++++++++++++++ zensical.toml | 13 +- 10 files changed, 1602 insertions(+), 2 deletions(-) create mode 100644 docs/learn/scans/fast-axis-slow-axis.md create mode 100644 docs/learn/scans/introduction.md create mode 100644 docs/learn/scans/learn-by-example.md create mode 100644 docs/learn/scans/position-generators.md create mode 100644 docs/learn/scans/scan-actions.md create mode 100644 docs/learn/scans/scan-components.md create mode 100644 docs/learn/scans/scan-definition-info.md create mode 100644 docs/learn/scans/scan-info.md create mode 100644 docs/learn/scans/scanargument.md diff --git a/docs/learn/scans/fast-axis-slow-axis.md b/docs/learn/scans/fast-axis-slow-axis.md new file mode 100644 index 0000000..7eadbb1 --- /dev/null +++ b/docs/learn/scans/fast-axis-slow-axis.md @@ -0,0 +1,131 @@ +--- +related: + - title: Position Generators + url: learn/scans/position-generators.md + - title: ScanArgument + url: learn/scans/scanargument.md + - title: Learn by Example + url: learn/scans/learn-by-example.md +--- + +# Fast Axis and Slow Axis + +When a scan moves more than one axis, the order of those axes matters. + +BEC follows one consistent convention: + +- the outermost axis is the slow axis +- the innermost axis is the fast axis + +That means the fast axis changes most often, while the slow axis changes only after the inner sweep +has finished. + +## What That Means In Practice + +A useful way to read the convention is: + +For every point in `samx` from `-5` to `5`, move `samy` from `-10` to `10`. + +In that example: + +- `samx` is the slow axis +- `samy` is the fast axis + +So the scan stays on one `samx` value while it sweeps through the full `samy` range, then advances +to the next `samx` value and repeats. + +!!! example + A 2D grid scan like this: + + ```py + scans.grid_scan(dev.samx, -5, 5, 3, dev.samy, -10, 10, 5, snaked=False, relative=False) + ``` + + would have `samx` as the slow axis and `samy` as the fast axis, so the scan would: + + 1. keep `samx` fixed at `-5` while it sweeps `samy` from `-10` to `10` + 2. advance `samx` to the next value (in this case `0`) while it again sweeps `samy` from `-10` to `10` + 3. advance `samx` to the next value (in this case `5`) while it again sweeps `samy` from `-10` to `10` + 4. finish the scan after the last `samx` value has been reached and its inner sweep has completed + + If the requirement is to have `samy` as the slow axis and `samx` as the fast axis, one would just swap the order of the motor arguments: + + ```py + scans.grid_scan(dev.samy, -10, 10, 5, dev.samx, -5, 5, 3, snaked=False, relative=False) + ``` + +## Why This Matters + +This convention affects how you read and define multi-axis scans: + +- the order of axes in a grid or nested scan is meaningful +- the generated point order follows that nesting +- snaking typically changes the traversal direction of the fast axis while keeping the same slow-axis structure + +Keeping that convention stable makes scan definitions easier to reason about and makes generated +point lists more predictable. + +## A Simple Example + +At the user level, a grid scan might look like this: + +```py +scans.grid_scan(dev.samx, -5, 5, 3, dev.samy, -10, 10, 5, snaked=False) +``` + +The same ordering appears in the generated positions: + +```py +positions = position_generators.nd_grid_positions( + [(-5.0, 5.0, 3), (-10.0, 10.0, 5)], + snaked=False, +) + +for point in positions: + samx_position = point[0] + samy_position = point[1] +``` + +Here the first axis is the outer loop and the second axis is the inner loop: + +- axis 1: slow axis +- axis 2: fast axis + +So the point order follows this pattern: + +1. keep the first axis fixed +2. sweep the second axis through all of its values +3. advance the first axis +4. repeat + +## How To Read Existing Scan Code + +When you see code such as: + +```py +positions = position_generators.nd_grid_positions( + [(start_motor1, stop_motor1, steps_motor1), (start_motor2, stop_motor2, steps_motor2)], + snaked=True, +) +``` + +read it as: + +- the first tuple defines the slow axis +- the second tuple defines the fast axis + +That same idea also applies more generally to nested point generation: outer definitions correspond +to slower-changing axes, and inner definitions correspond to faster-changing axes. + +## Next Step + +After axis-order conventions, continue with [ScanArgument](scanargument.md). + +That page covers the rich input metadata used in scan signatures. + +## What To Remember + +!!! info "What to remember" + - In BEC, the outermost axis is the slow axis and the innermost axis is the fast axis. + - The fast axis changes most often within the generated point list. + - This convention makes multi-axis scan definitions and point ordering easier to read. diff --git a/docs/learn/scans/introduction.md b/docs/learn/scans/introduction.md new file mode 100644 index 0000000..581966e --- /dev/null +++ b/docs/learn/scans/introduction.md @@ -0,0 +1,128 @@ +--- +related: + - title: System architecture overview + url: learn/system-architecture/overview/index.md + - title: File writing + url: learn/file-writer/introduction.md + - title: Access BEC history + url: how-to/scans/access-bec-history.md +--- + +# Scans in BEC + +!!! Info "Overview" + Scans are the core of BEC's functionality. They are the tools that move your devices, trigger readouts, and produce the data you analyze. In BEC, all scans follow a shared structure and report themselves in a consistent way, even when their motion logic differs. + +BEC scans follow one shared model. Whether you run a simple acquisition, a line scan, a grid scan, +or a continuous scan, BEC handles them in the same overall way. + + +## The Main Idea + +The most important idea in BEC scan execution is simple: + +- all scans follow the same overall structure +- all scans are reported through the same backend model +- all scans are executed on the server +- all scans produce data that can be accessed in the same general way afterward +- the client learns the available scans from the scan server, including their current signatures and metadata + +That is true even when the middle of the scan is very different. + +For example, a line scan, a grid scan, and a continuous scan may move differently, but they still fit into one common scan framework. + +## What Happens During A Scan + +!!! Note "Dataflow during a scan" + A general overview of the dataflow in BEC can be found in the [system architecture overview](../../learn/system-architecture/overview/data-flow.md){ data-preview }. + +At a high level, a scan in BEC follows this path: + +1. The scan server publishes the available scan classes together with their serialized signatures, grouped inputs, and GUI metadata. +2. The client exposes those scans dynamically, so commands such as `scans.line_scan(...)` use the current server-side definition. +3. When a scan is called, the client validates the user inputs, resolves device-name strings to device objects where needed, and bundles repeated positional inputs for scans such as `line_scan` or `umv` before sending the request to the server. +4. On the server, the request is queued and receives runtime identifiers. +5. Once it is the scan's turn to run, the scan server queue hands over the request to a scan worker. +6. The scan worker runs the scan class's lifecycle hooks and sends device instructions through Redis. +7. Devices publish readouts and status updates. +8. The scan bundler groups those readouts into logical scan points. +9. Clients, history, and the file writer consume the resulting scan data. + +From the user side, the important part is consistency: every scan goes through the same backend +steps, reports its progress in the same general way, and produces scan data that can be accessed +through the same tools afterward. + +## The Shared Scan Shape + +As mentioned above, all BEC scans follow the same overall shape. They share the same lifecycle steps, and they use the same helpers to report themselves and produce data. This allows users to be always prompted with a familiar structure, even when the motion logic of the scan differs. The shared shape also makes it easier to learn new scans, since you can focus on the differences in motion logic rather than having to learn a new overall structure for each scan. The lifecycle steps are: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HookRole
prepare_scanPrepare the scan for the upcoming acquisition.
open_scanOpen the scan and emit a new scan status message with all relevant metadata.
stageStage the devices for the upcoming acquisition.
pre_scanRun any pre-scan logic, such as preparing time-critical devices.
scan_coreRun the core logic of the scan; trigger readouts if needed and optionally call at_each_point() for per-point logic.
post_scanRun any post-scan logic, such as moving devices back to their original position.
unstageUnstage the devices.
close_scanClose the scan and emit a final scan status message with all relevant metadata.
on_exceptionIf an exception is raised during any earlier step, run cleanup logic here.
+ +BEC runs these lifecycle steps in the same order every time. + +This shared shape is why BEC scans feel related rather than like separate one-off tools. + +## Where To Go Next + +If you want the next layer of detail: + +- read [Learn by example](../learn/scans/learn-by-example.md){ data-preview } to go through the `acquire` scan example in detail, and see how the shared scan shape applies to a specific scan type. + +## What to Remember + +!!! info "What to remember" + - In BEC, all scans follow the same overall shape. + - Different scan types use the same backend framework, even when their motion logic differs. + - Every scan reports itself in a common structured way while it runs. + - Every scan has to implement the same lifecycle steps, even when some of those steps are empty. + - The lifecycle steps are called in the same order for every scan. diff --git a/docs/learn/scans/learn-by-example.md b/docs/learn/scans/learn-by-example.md new file mode 100644 index 0000000..c3ffeed --- /dev/null +++ b/docs/learn/scans/learn-by-example.md @@ -0,0 +1,218 @@ +--- +related: + - title: Introduction to Scans + url: learn/scans/introduction.md + - title: Scan Lifecycle + url: learn/scans/lifecycle.md + - title: Scan Actions + url: learn/scans/scan-actions.md + - title: Scan Components + url: learn/scans/scan-components.md + - title: Scan Info + url: learn/scans/scan-info.md + - title: ScanArgument + url: learn/scans/scanargument.md + - title: Scan Definition Info + url: learn/scans/scan-definition-info.md +--- + +# Learn by Example + +This page walks through a small BEC scan implementation and points out where the shared scan +shape becomes scan-specific. + +The example used here is `acquire`. It is much shorter than a motor-driven scan, which makes the +fixed lifecycle easier to see before moving on to more complex scan types. + +## Why This Example + +`acquire` is a good first example because it shows the full scan lifecycle without adding motor +trajectory logic on top. + +It still demonstrates the main parts of a real scan: + +- a compact `__init__` with `ScanArgument` metadata +- `gui_config` for graphical clients +- a small but meaningful `prepare_scan` +- a short `scan_core` +- explicit cleanup in `post_scan` and `close_scan` + +## The Class Definition and Inputs + +The first part of the class tells BEC what kind of scan this is, how GUIs should present it, and +what inputs it accepts. + +```py +from __future__ import annotations + +from typing import Annotated + +from bec_lib.scan_args import ScanArgument, Units +from bec_server.scan_server.scans import ScanBase, ScanType +from bec_server.scan_server.scans.scan_modifier import scan_hook + + +class Acquire(ScanBase): + scan_type = ScanType.SOFTWARE_TRIGGERED # (1)! + scan_name = "acquire" # (2)! + + gui_config = { # (3)! + "Acquisition Parameters": [ + "exp_time", + "frames_per_trigger", + "settling_time", + "readout_time", + ], + } + + def __init__( + self, + exp_time: Annotated[ + float, ScanArgument(display_name="Exposure Time", units=Units.s, ge=0) + ] = 0, # (4)! + frames_per_trigger: Annotated[ + int, ScanArgument(display_name="Frames per Trigger", ge=1) + ] = 1, + settling_time: Annotated[ + float, ScanArgument(display_name="Settling Time", units=Units.s, ge=0) + ] = 0, + readout_time: Annotated[ + float, ScanArgument(display_name="Readout Time", units=Units.s, ge=0) + ] = 0, + **kwargs, # (5)! + ): + super().__init__(**kwargs) # (6)! + self.exp_time = exp_time + self.frames_per_trigger = frames_per_trigger + self.settling_time = settling_time + self.readout_time = readout_time + + self.update_scan_info( # (7)! + exp_time=exp_time, + frames_per_trigger=frames_per_trigger, + settling_time=settling_time, + readout_time=readout_time, + ) +``` + +1. `scan_type` tells BEC that this scan's main logic is software-triggered. +2. `scan_name` is the name published by the scan server and exposed to the client. It must be unique across all scans and a valid Python identifier. Once loaded, the scan is available as `scans.(...)` on the client. +3. The `gui_config` dictionary groups and enables input fields in graphical clients. In this case, all inputs are grouped under "Acquisition Parameters". Keys that don't appear in `gui_config` are still valid inputs from the command-line but they won't be shown in GUIs. +4. `ScanArgument(...)` carries labels, units, and validation bounds. In this case, the `exp_time` argument must be a float and the `ScanArgument` further specifies that it should be displayed as "Exposure Time" with units of seconds and the value must be greater than or equal to (ge) 0. +5. `**kwargs` is needed to be able to make the scan connect to BEC's scan lifecycle and forward additional metadata to `ScanBase`. +6. The `super().__init__(**kwargs)` call is required to properly initialize the scan base class and give the scan access to devices, actions, components, and the scan info object. It usually doesn't need to receive any additional arguments, only `**kwargs`. +7. Use `update_scan_info(...)` to update any standard scan metadata field that depends on the scan inputs. This is important as it is the scan info object that is used for broadcasting information about the scan to clients, devices, and the file writer. In this case, the acquisition parameters are added to the scan info for later reference. + + +## `prepare_scan`: Small But Still Important + +Even a short scan usually uses `prepare_scan` to finalize metadata and publish progress reporting. + +```py +@scan_hook +def prepare_scan(self): + self.update_scan_info( # (1)! + num_points=1, + num_monitored_readouts=self.frames_per_trigger, + ) + + self.actions.add_scan_report_instruction_scan_progress( # (2)! + points=self.scan_info.num_monitored_readouts, + show_table=False, + ) + + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) # (3)! +``` + +1. The scan declares its final runtime shape: one acquisition step with a known readout count. +2. The progress instruction lets clients render consistent progress even for a simple scan. +3. Baseline readout starts early and can finish in parallel with the rest of scan setup. + +This is useful to compare with a motor scan: the hook is still important, but it is shorter because +there are no positions to generate, shift, or limit-check. + +## The Shared Lifecycle Around The Acquisition + +After `prepare_scan`, the scan looks much like any other BEC scan again. + +```py +@scan_hook +def open_scan(self): + self.actions.open_scan() + +@scan_hook +def stage(self): + self.actions.stage_all_devices() + +@scan_hook +def pre_scan(self): + self.actions.pre_scan_all_devices() # (1)! + +@scan_hook +def scan_core(self): + for _ in range(self.frames_per_trigger): # (2)! + self.components.trigger_and_read() # (3)! +``` + +1. `pre_scan_all_devices()` runs the usual last-moment device preparation. +2. `frames_per_trigger` still affects the acquisition loop, even though nothing is moving. +3. The scan reuses the shared trigger-and-read helper instead of implementing detector logic itself. + +This is the key lesson of the `acquire` example: even the shortest useful scan still fits the same +hook order and still delegates repetitive device work to shared helpers. + +## Cleanup + +The final hooks show how the scan finishes cleanly. + +```py +@scan_hook +def post_scan(self): + self.actions.complete_all_devices() # (1)! + +@scan_hook +def unstage(self): + self.actions.unstage_all_devices() + +@scan_hook +def close_scan(self): + if self._baseline_readout_status is not None: # (2)! + self._baseline_readout_status.wait() + self.actions.close_scan() + self.actions.check_for_unchecked_statuses() # (3)! +``` + +1. `post_scan` lets devices finish their acquisition-side work before teardown. +2. `close_scan` waits for the asynchronous baseline readout to finish. +3. The final status check helps catch unfinished device operations. + +This cleanup path is shorter than in a relative motor scan, because there are no motors to move +back and no point-pattern state to unwind. + +## What This Example Shows + +The `acquire` scan is a compact example of the shared scan shape: + +- the hook order is unchanged +- `prepare_scan` is still where final scan metadata is prepared +- `scan_core` stays short because it composes shared helpers +- scan-definition metadata such as `gui_config` and `ScanArgument` lives alongside the code +- even a simple scan still needs explicit cleanup + +## Next Step + +After this example, the next useful topic is [scan info](scan-info.md). + +That page explains the shared metadata object this scan keeps updating through +`update_scan_info(...)`. + +After `scan info`, continue with [scan actions](scan-actions.md), and then move on to +[scan components](scan-components.md) for `components`. + +## What to Remember + +!!! info "What to remember" + - This page uses `acquire` because it shows the shared scan shape with minimal extra logic. + - The scan-specific parts are mostly in `__init__`, `prepare_scan`, and the short acquisition loop in `scan_core`. + - The repetitive execution path is delegated to shared helpers in `actions` and `components`. + - More complex scans keep the same lifecycle, but add motion planning and richer cleanup on top. diff --git a/docs/learn/scans/position-generators.md b/docs/learn/scans/position-generators.md new file mode 100644 index 0000000..bec20b8 --- /dev/null +++ b/docs/learn/scans/position-generators.md @@ -0,0 +1,181 @@ +--- +related: + - title: Scan Components + url: learn/scans/scan-components.md + - title: Learn by Example + url: learn/scans/learn-by-example.md + - title: Fast Axis and Slow Axis + url: learn/scans/fast-axis-slow-axis.md +--- + +# Position Generators + +Many scans need a prepared list of points before the scan can start moving hardware. + +BEC keeps that logic in `position_generators`: a collection of reusable helpers that generate +positions for common scan patterns. A scan typically uses these helpers in `prepare_scan`, then +records the resulting positions in `scan_info`, checks limits, and lets `components` handle the +repeated execution pattern. + +## Why Position Generators Matter + +Position generation is often one of the most scan-specific parts of a scan. + +For example, a scan may need to produce: + +- a straight line through one or more axes +- a rectangular grid, optionally with snaking +- a spiral or Fermat pattern +- a circular shell pattern +- several disconnected regions combined into one trajectory + +Keeping those patterns in reusable helpers makes scan classes shorter and easier to read. + +## Common Position Generators + +Some of the most commonly used helpers are listed below. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HelperRoleTypical scan type
line_scan_positions(...)Builds linearly spaced points for one or more axes.line scans
log_scan_positions(...)Builds positions with logarithmically increasing spacing between start and stop.logarithmic line scans
nd_grid_positions(...)Builds an N-dimensional grid, with optional snaking to reduce unnecessary travel.grid scans
multi_region_line_positions(...)Builds one 1D trajectory across several separate line regions.multi-region line scans
multi_region_grid_positions(...)Builds several rectangular sub-grids and concatenates them into one scan path.multi-region grid scans
spiral_positions(...)Builds an Archimedean spiral clipped to a rectangular region.spiral scans
fermat_spiral_pos(...)Builds a Fermat spiral inside rectangular scan bounds.Fermat spiral scans
round_scan_positions(...)Builds concentric circular shells around a center point.round scans
get_round_roi_scan_positions(...)Builds circular shell points and clips them to a rectangular region of interest.round ROI scans
hex_grid_2d(...)Builds a 2D hexagonal grid inside the requested scan bounds.hexagonal scans
oscillating_positions(...)Yields values in a back-and-forth pattern instead of returning a finite point array.hysteresis or oscillating scans
rotate_points(...)Rotates a 2D point set around a chosen center.supporting helper
+ +## Position Array Shape + +For helpers that return a finite point list, the result is a NumPy array of shape +`(num_points, num_motors)`. Each row is one scan point, and each column corresponds to one motor or +scanned axis. For single-axis scans, that usually means shape `(num_points, 1)`. + +For example, a two-motor grid scan might generate and consume positions like this: + +```py +positions = position_generators.nd_grid_positions( + [(-1.0, 1.0, 3), (-2.0, 2.0, 5)], + snaked=True, +) + +for point in positions: + motor1_position = point[0] + motor2_position = point[1] +``` + +## How Scans Use Them + +The usual pattern is: + +1. use a position generator in `prepare_scan` +2. optionally shift those points for relative motion +3. check limits against the final point list +4. store the positions and point count in `scan_info` +5. pass the prepared positions into `components.step_scan(...)` or another execution helper + +This keeps the scan code split into clear responsibilities: + +- position generators decide where the scan should go +- `actions` handles lifecycle and reporting +- `components` handles the repeated execution pattern + +If the scan wants to improve the traversal order after generating the points, it can then call +`components.optimize_trajectory(...)` before execution begins. This is useful when the point +generator defines which points should be visited, but the scan still wants to reduce unnecessary +travel between those points. + +## When Not To Use One + +Not every scan needs a position generator. + +For example: + +- `acquire` does not move through a point list at all +- monitor-style scans may react to live updates instead of a precomputed trajectory +- some scans generate their next point on the fly rather than preparing the full array up front + +So position generators are common, but they are most useful when the scan can describe its path in +advance. + +## Next Step + +After position generators, continue with [fast axis and slow axis](fast-axis-slow-axis.md). + +That page explains the axis-order convention used when scans generate points for more than one +motor. + +## What To Remember + +!!! info "What to remember" + - Position generators build reusable point lists for common scan geometries. + - They are most often used in `prepare_scan` before limit checks and execution begin. + - They keep scan classes shorter by separating path generation from execution logic. diff --git a/docs/learn/scans/scan-actions.md b/docs/learn/scans/scan-actions.md new file mode 100644 index 0000000..6fda40a --- /dev/null +++ b/docs/learn/scans/scan-actions.md @@ -0,0 +1,216 @@ +--- +related: + - title: Scan Info + url: learn/scans/scan-info.md + - title: Scan Components + url: learn/scans/scan-components.md + - title: Learn by Example + url: learn/scans/learn-by-example.md +--- + +# Scan Actions + +`self.actions` is the high-level interface a scan uses to interact with devices and publish +scan-related state. + +In practice, `self.actions` is an instance of `ScanActions`. These methods +wrap common device-server instructions, track their statuses, and update scan-report state so that +concrete scans do not need to reimplement the same coordination logic again and again. + +## What `actions` Is For + +`actions` is typically used for three kinds of work: + +- lifecycle orchestration such as opening, staging, and closing a scan +- device operations such as moving, triggering, reading, and completing +- reporting and metadata updates such as progress instructions and readout-priority changes + +The important design point is that `actions` stays scan-facing and task-oriented. A scan usually +asks for a high-level operation like `stage_all_devices()` or `read_baseline_devices()` rather than +building instruction messages manually. + +## Common `ScanActions` Methods + +The current public `ScanActions` API includes the following methods most relevant to scan authors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRoleTypical hook
open_scan()Publishes the opening scan status message for the current scan.open_scan
stage_all_devices()Stages all enabled scan devices, with async devices handled separately for better throughput.stage
stage(...)Stages one device or a selected device list instead of the whole scan device set.stage in custom cases
pre_scan_all_devices()Runs the pre-scan device step across all enabled devices.pre_scan
pre_scan(...)Runs the pre-scan device step only for selected devices.pre_scan in custom cases
set(...)Sends coordinated set instructions to one or several devices.prepare_scan or scan_core
kickoff(...)Starts a kickoff-capable device with optional configuration parameters.scan_core or flyer setup
complete(...)Completes one device explicitly.post_scan
complete_all_devices()Completes all enabled scan devices.post_scan
read_monitored_devices()Reads the current monitored-device group and advances the monitored readout counter.scan_core
read_manually(...)Performs an explicit read and returns the result to the scan, rather than relying on the usual monitored-read path.Special cases only
publish_manual_read(...)Publishes externally collected data as the next monitored readout.Special cases only
read_baseline_devices()Reads the baseline-device group, often around scan setup or teardown.Usually prepare_scan
trigger_all_devices()Triggers all devices configured for software triggering in the scan.scan_core
unstage(...)Unstages one selected device.unstage in custom cases
unstage_all_devices()Unstages all enabled scan devices.unstage
add_scan_report_instruction_scan_progress(...)Adds a scan-progress instruction so clients can render scan progress consistently.Usually prepare_scan
add_scan_report_instruction_readback(...)Adds a live readback instruction for selected devices.Usually prepare_scan
add_scan_report_instruction_device_progress(...)Adds a device-progress instruction for devices exposing progress signals.Usually prepare_scan
set_device_readout_priority(...)Modifies which devices are treated as monitored, baseline, on-request, or async during the scan.Usually __init__ or prepare_scan
close_scan()Finalizes monitored-readout counts, checks cleanup state, and publishes the closing scan status.close_scan
check_for_unchecked_statuses()Warns about unfinished or unchecked status objects and waits on remaining work when needed.close_scan or cleanup
add_device_with_required_response(...)Marks devices whose instructions must emit explicit response messages.Special cases only
rpc_call(...)Makes a low-level RPC call to a device-server method and returns the result.Advanced cases only
send_client_info(...)Sends an informational message to clients, for example for GUI status updates.Any hook when useful
+ +## How `actions` Fits With `scan_info` + +Many `actions` methods do more than send device instructions. + +For example: + +- `add_scan_report_instruction_*` updates `scan_info.scan_report_instructions` +- `set_device_readout_priority(...)` updates `scan_info.readout_priority_modification` +- `close_scan()` writes back the actual `num_monitored_readouts` before publishing the closing status + +So `actions` is one of the main ways a concrete scan both performs work and keeps its shared runtime +description synchronized. + +## When To Prefer `actions` + +Prefer `actions` when you want to express an operation in scan terms: + +- open or close the scan +- stage or unstage devices +- trigger or read the configured scan device groups +- publish scan progress or readback instructions +- adjust scan-local readout priorities + +This keeps scan code short and consistent. + +If you need reusable lower-level scan logic such as step-scan execution, motor limit checking, or +move-and-wait helpers, that belongs in `components`. + +## Next Step + +After `actions`, continue with [scan components](scan-components.md). + +## What To Remember + +!!! info "What to remember" + - `actions` is the scan-facing helper for lifecycle operations, device instructions, and reporting updates. + - Most concrete scans should prefer `actions` over building instruction messages manually. + - Several `actions` methods also update `scan_info`, not just device state. diff --git a/docs/learn/scans/scan-components.md b/docs/learn/scans/scan-components.md new file mode 100644 index 0000000..e03d283 --- /dev/null +++ b/docs/learn/scans/scan-components.md @@ -0,0 +1,165 @@ +--- +related: + - title: Scan Actions + url: learn/scans/scan-actions.md + - title: Position Generators + url: learn/scans/position-generators.md + - title: ScanArgument + url: learn/scans/scanargument.md + - title: Scan Info + url: learn/scans/scan-info.md +--- + +# Scan Components + +`self.components` contains reusable scan logic for common scan patterns. + +Where `actions` gives a scan high-level operations such as staging, reading, or publishing progress, +`components` builds on top of those operations to make frequently used scan patterns easier to +reuse. + +## Why Scan Components Matter + +Many scans need the same kinds of logic: + +- loop over prepared positions +- move devices and wait for motion to finish +- trigger and read at each point +- capture starting positions for relative scans +- check limits before motion begins + +Without shared components, each scan would need to reimplement that behavior for itself. + +`components` exists so scans can reuse those patterns directly and stay focused on what is actually +scan-specific. + +## `actions` Versus `components` + +The difference is mostly one of level. + +- `actions` provides scan-facing operations such as `stage_all_devices()`, `read_baseline_devices()`, or `set(...)` +- `components` combines those operations into reusable scan patterns such as step scans, trigger-and-read sequences, or move-and-wait flows + +That is why components are best understood as reusable building blocks on top of `actions`. + +A concrete scan will often use both: + +- `actions` for lifecycle work and reporting +- `components` for the repeated motion and acquisition pattern inside `prepare_scan` or `scan_core` + +## Common `ScanComponents` Helpers + +Some of the most commonly used helpers are listed below. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HelperRoleTypical hook
step_scan(...)Runs the standard repeated move/trigger/read pattern for prepared point lists.scan_core
step_scan_at_each_point(...)Handles the usual per-point logic used inside a step scan.at_each_point
trigger_and_read()Runs the common trigger/read sequence without rewriting the device coordination each time.scan_core
move_and_wait(...)Moves devices and waits for motion to complete before continuing.prepare_scan or post_scan
get_start_positions(...)Captures the starting device positions, for example for relative scans.prepare_scan
check_limits(...)Checks planned motion against device limits before the scan starts moving hardware.prepare_scan
optimize_trajectory(...)Reorders prepared positions to reduce unnecessary motion for scans that benefit from path optimization.prepare_scan
+ +The important point is not to memorize every helper name. The important point is that common scan +behavior is reused instead of being rewritten in every scan class. + +## A Typical Example + +In a step scan, the scan-specific code often looks something like this: + +1. prepare the positions +2. check limits +3. update `scan_info` +4. let `components.step_scan(...)` handle the repeated move/trigger/read sequence + +That is a cleaner pattern than manually open-coding every move, wait, trigger, and read inside the +scan class. + +This is also why components help readability: once you know the shared helpers, you can see more +quickly which parts of a scan are generic and which parts are genuinely specific to that scan. + +## Two Common Execution Styles + +The same framework supports both software-triggered and hardware-triggered scans. + +### Software-triggered scans + +Scans such as `acquire`, `time_scan`, `line_scan`, and `grid_scan` usually follow a simple pattern: + +1. move to the next position if needed +2. wait for motion to finish +3. apply settling time +4. trigger devices +5. read monitored devices + +In many cases this logic is handled through `components.step_scan(...)` and +`components.trigger_and_read()`. + +### Hardware-triggered or continuous scans + +Scans such as `cont_line_fly_scan` or `monitor_scan` use the same lifecycle, but their +`scan_core` hook looks different: + +- the motor may move continuously between only a start and stop point +- per-point work may be driven by readback updates or by repeated trigger/read cycles while motion is still active +- progress is often reported through readback instructions rather than a fixed point table + +So the framework stays the same, but the reused execution pattern changes to suit the scan style. + +## Next Step + +After `components`, continue with [position generators](position-generators.md). + +That page covers the reusable helpers used to build point lists and trajectories before the scan +execution pattern begins. + +## What To Remember + +!!! info "What to remember" + - `components` contains reusable scan patterns built on top of `actions`. + - Components help scans reuse common motion and acquisition logic more elegantly. + - Components are most useful when a scan needs a familiar execution pattern without reimplementing it from scratch. diff --git a/docs/learn/scans/scan-definition-info.md b/docs/learn/scans/scan-definition-info.md new file mode 100644 index 0000000..b60c953 --- /dev/null +++ b/docs/learn/scans/scan-definition-info.md @@ -0,0 +1,129 @@ +# Scan Definition Info + +This page explains the extra information a scan class can define so that the rest of BEC can present +and understand the scan correctly. + +## The Scan Signature + +In the current scan implementation, a scan definition is described largely by its `__init__` +signature plus a few class attributes. + +The scan server serializes that signature and publishes it to clients. The client then uses it to: + +- expose the scan under `scans.` +- attach a live Python signature in IPython +- validate kwargs and bundled positional inputs +- resolve device-name strings to device objects when the annotations require that + +This means the signature is no longer only local Python documentation. It is part of the runtime API +contract between the scan server, the client, and GUIs. + +## `arg_input` and `arg_bundle_size` + +!!! tip + `arg_input` and `arg_bundle_size` are special cases for scans with an undefined number of + input arguments. Most scans developed in plugins do not need them. + +If the number of input arguments is not fixed, the usual Python-style fixed signature is not enough. + +For example, a line scan can work with any number of motors in parallel, so the scan cannot rely on +one fixed positional argument layout in the way an ordinary Python function usually would. + +In those cases, scans with repeated positional bundles declare those bundles explicitly. + +For a line scan, that can look like this: + +```py +arg_input = { + "device": DeviceBase, + "start": float, + "stop": float, +} +arg_bundle_size = {"bundle": 3, "min": 1, "max": None} +``` + +`arg_bundle_size` then tells BEC how many positional values belong to one bundle and how many +bundles are allowed. + +This is what lets BEC validate a call such as: + +```py +scans.line_scan(dev.samx, -1, 1, dev.samy, -2, 2, steps=5, relative=False) +``` + +without treating those positional arguments as an unstructured `*args` blob. + +Rich input metadata for individual parameters is covered separately on +[ScanArgument](scanargument.md). This page focuses on the broader scan-definition structure around +the signature, grouped positional bundles, and GUI grouping. + +## `gui_config` + +`gui_config` describes how scan inputs should be grouped in graphical interfaces. + +For example, a scan can group inputs under headings such as: + +- `Device` +- `Movement Parameters` +- `Acquisition Parameters` + +This does not change how the scan runs. It helps GUIs present scan inputs in a clear structure +instead of showing one flat list of parameters. + +## Unit Annotations + +Many scans annotate numerical inputs through `Annotated[..., ScanArgument(...)]`. + +Typical examples are: + +- `Annotated[float, ScanArgument(units=Units.s)]` for seconds +- `Annotated[float, ScanArgument(units=Units.eV)]` for electron volts +- `Annotated[float, ScanArgument(units=Units.deg)]` for degrees + +These annotations make the intended unit explicit in the scan definition. + +Scans can also reference the unit of another input directly. For example: + +- `Annotated[float, ScanArgument(reference_units="motor1")]` +- `Annotated[float, ScanArgument(reference_units="motor2")]` +- `Annotated[float, ScanArgument(reference_units="device")]` + +This means the value should use the same unit as that referenced input. So +`Annotated[float, ScanArgument(reference_units="motor1")]` means “interpret this value in the +units of `motor1`.” + +This is especially useful for position-like parameters such as starts, stops, and step sizes. + +## `scan_def` and `scan_group` + +Two pieces of scan-definition-related metadata are now handled on the client side and propagated in +the request `system_config`: + +- `scans.scan_def`, which creates a temporary scan-definition ID and wraps several scan requests into one logical definition +- `scans.scan_group`, which assigns several scan requests to one queue group + +These are context managers and decorators. They do not replace the scan class definition itself, but +they are part of how modern BEC associates related scan requests at runtime. + +## Reloading The Scan Server + +If you add a new scan or change an existing scan class, the scan server must reload that Python code +before the changes become available. + +In practice, that means you should restart or reload the scan server after editing scan +implementations. Otherwise the running server will continue using the old version of the scan. + +## Next Step + +If you want to see these ideas in a richer real scan, read the +[worked example: hexagonal scan](hexagonal-scan-example.md). + +## What To Remember + +!!! info "What to remember" + - The serialized scan signature is part of the runtime API between the scan server, clients, and GUIs. + - `arg_input` and `arg_bundle_size` define repeated positional bundles such as move targets or line-scan ranges. + - `ScanArgument` carries units, bounds, labels, and other GUI-facing metadata for individual inputs. + - `gui_config` helps GUIs present scan parameters clearly. + - Unit annotations make scan inputs more explicit and can also refer to the units of another input. + - After changing scan code, the scan server must be reloaded or restarted. diff --git a/docs/learn/scans/scan-info.md b/docs/learn/scans/scan-info.md new file mode 100644 index 0000000..7e914a0 --- /dev/null +++ b/docs/learn/scans/scan-info.md @@ -0,0 +1,264 @@ +--- +related: + - title: Learn by Example + url: learn/scans/learn-by-example.md + - title: Scan Actions + url: learn/scans/scan-actions.md + - title: Scan Components + url: learn/scans/scan-components.md + - title: Introduction to Scans + url: learn/scans/introduction.md +--- + +# Scan Info + +`scan_info` is the runtime metadata object attached to every scan. + +In the current implementation, `self.scan_info` is an instance of the `ScanInfo` model created by +`ScanBase` when the scan object is initialized. The scan then keeps refining that object during +`prepare_scan` and the later lifecycle hooks. + +This is the authoritative internal description of the running scan. The queue, scan actions, +clients, bundler, and file writer all rely on it directly or on message payloads derived from it. + +## What `scan_info` Is For + +Without one shared runtime model, each part of BEC would need to reconstruct the scan from a mix of +request arguments, device instructions, and status messages. + +`scan_info` solves that by keeping the scan description in one structured object, including: + +- what scan is running +- how many points or monitored readouts it expects +- which timing parameters apply +- which devices should be reported +- which request inputs the user originally sent +- which extra scan-specific parameters should travel with the scan + +## The `ScanInfo` Model + +The current `ScanInfo` model contains the following fields. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldRoleTypically set by
scan_nameName of the scan class published to the client, for example acquire.ScanBase from the class attribute
scan_idUnique runtime identifier for this scan instance.Queue and worker setup
scan_typeInternal scan type, currently software_triggered or hardware_triggered.ScanBase from the class attribute
scan_numberAssigned scan number for the run, if available.Queue and runtime bookkeeping
dataset_numberAssigned dataset number for the run, if available.Queue and runtime bookkeeping
num_pointsNumber of logical scan points.Usually prepare_scan
positionsPrepared position array for scans that precompute positions.Usually prepare_scan
exp_timeExposure time for the scan.__init__ or update_scan_info(...)
frames_per_triggerNumber of frames collected per trigger.__init__ or update_scan_info(...)
settling_timeSettling delay before a software trigger.__init__ or update_scan_info(...)
settling_time_after_triggerSettling delay after a software trigger.__init__ or update_scan_info(...)
readout_timeReadout delay after triggering.__init__ or update_scan_info(...)
burst_at_each_pointHow many bursts are collected at each point.__init__ or update_scan_info(...)
relativeWhether prepared positions are interpreted relative to the current device state.__init__ or update_scan_info(...)
run_on_exception_hookWhether the scan should run its on_exception cleanup hook on interruption.Base initialization or later update
request_inputsStructured copy of the original request inputs sent by the client.ScanBase initialization
readout_priority_modificationRequested overrides to device readout priority during the scan.Usually helper calls in actions
scan_report_instructionsUI/report instructions such as progress widgets or readback displays.Usually helper calls in actions
scan_report_devicesDevices highlighted in scan reports. Device objects are stored by name.Usually update_scan_info(...)
monitor_syncMonitor synchronization mode for fly scans. This field is marked for removal.Scan-specific logic when needed
additional_scan_parametersExtra scan-specific parameters that do not have dedicated top-level fields.Unknown keys passed to update_scan_info(...)
user_metadataUser-provided metadata attached to the request.Request setup
system_configSystem-side configuration relevant to the scan, such as file-writing settings.Request setup
scan_queueName of the queue this scan belongs to.Request setup
metadataAdditional runtime metadata associated with the scan request.Request setup and runtime bookkeeping
num_monitored_readoutsTotal number of monitored readouts expected for the run.Usually prepare_scan
+ +## How Scans Fill It + +The important pattern is simple: + +1. `ScanBase` creates the initial `scan_info` object from stable request and class metadata. +2. The concrete scan updates known runtime fields through `update_scan_info(...)`. +3. Helper methods in `self.actions` also update related reporting state such as scan-report instructions and readout-priority overrides. + +`update_scan_info(...)` has one especially important behavior: if you pass a keyword that matches a +real `ScanInfo` field, that field is updated directly. If you pass a keyword that does not exist on +the model, it is stored under `additional_scan_parameters` instead. + +That means scans can attach extra scan-specific metadata without having to extend the shared model +for every new scan type. + +## `request_inputs` + +`request_inputs` stores the original request in structured form. + +BEC keeps three buckets here: + +- `arg_bundle` for repeated positional bundles +- `inputs` for fixed signature-bound inputs +- `kwargs` for extra keyword-style inputs + +This matters because the system no longer needs to reconstruct the user's request from a flattened +argument list later on. GUIs, history, file writing, and diagnostics can all refer back to the same +structured input representation. + +## `scan_info` Versus Published Scan Status Messages + +`scan_info` is the internal runtime model, but the public scan status messages are compatibility +views built from it. + +In practice: + +- the scan status message top level exposes a smaller legacy-style subset such as `scan_name`, `num_points`, `scan_parameters`, and `request_inputs` +- the `info` payload contains a full dumped view of `scan_info`, plus compatibility fields such as resolved `readout_priority` and `file_components` +- the internal `scan_info.scan_type` values are `software_triggered` and `hardware_triggered`, while the legacy top-level message field may only contain older values such as `step` or `fly` + +So when reading older code or older docs, keep the distinction clear: `scan_info` is now the +authoritative in-process model, and some message fields are compatibility projections of it rather +than the model itself. + +## Why `prepare_scan` Matters So Much + +`prepare_scan` is often where the scan's final runtime description becomes complete. + +That is usually where a scan: + +- finalizes `num_points` +- writes prepared `positions` +- computes `num_monitored_readouts` +- records timing or movement options +- publishes scan-report instructions +- selects report devices + +This is why `scan_info` shows up so often in real scan implementations: it is the shared place +where the scan turns its user inputs into a concrete runtime description. + +## Next Step + +After `scan_info`, continue with [scan actions](scan-actions.md). + +That page covers the high-level scan operations used to publish scan state and coordinate device +work. After that, move on to [scan components](scan-components.md) for the reusable scan patterns +built on top of those operations. + +## What To Remember + +!!! info "What to remember" + - `scan_info` is the shared runtime metadata model for a scan. + - `ScanBase` creates it, and concrete scans usually finish populating it during `prepare_scan`. + - Known updates go to named `ScanInfo` fields; unknown updates go to `additional_scan_parameters`. + - Published scan status messages are derived from `scan_info`, but they are not a 1:1 copy of the internal model. diff --git a/docs/learn/scans/scanargument.md b/docs/learn/scans/scanargument.md new file mode 100644 index 0000000..52d4daa --- /dev/null +++ b/docs/learn/scans/scanargument.md @@ -0,0 +1,159 @@ +--- +related: + - title: Position Generators + url: learn/scans/position-generators.md + - title: Scan Definition Info + url: learn/scans/scan-definition-info.md + - title: Learn by Example + url: learn/scans/learn-by-example.md +--- + +# ScanArgument + +`ScanArgument(...)` is the main way a scan attaches rich metadata to one of its inputs. + +In practice, this metadata is usually carried through `Annotated[..., ScanArgument(...)]` in a +scan's `__init__` signature. That makes the input definition useful not only to Python, but also to +validation, GUIs, and client-side scan discovery. + +## Why `ScanArgument` Matters + +Without `ScanArgument`, a scan input would mostly just have a Python type. + +With `ScanArgument`, the same input can also describe: + +- how it should be labeled in a GUI +- which units it uses +- whether it should use the units of another input +- which bounds or limits apply +- which extra explanatory text should be shown to the user + +That is what lets one scan signature serve as both an implementation interface and a user-facing +definition. + +## A Typical Example + +```py +from typing import Annotated + +from bec_lib.scan_args import ScanArgument, Units + + +exp_time: Annotated[ + float, + ScanArgument(display_name="Exposure Time", units=Units.s, ge=0), +] = 0 +``` + +This says several things at once: + +- the input should be treated as a `float` +- GUIs should label it as `Exposure Time` +- the value is expressed in seconds +- the value must be greater than or equal to zero + +## Common `ScanArgument` Fields + +Some of the most commonly used fields are listed below. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldRoleTypical use
display_nameProvides a clearer user-facing label than the raw Python parameter name.GUI labels
descriptionProvides a longer explanation of what the input means.help text or documentation
tooltipAdds short explanatory text for interactive UIs.GUI hover text
unitsDeclares the explicit unit for the input, such as seconds or degrees.timing or physical values
reference_unitsTells BEC to interpret the value in the units of another input, such as a motor argument.position-like inputs
gt, ge, lt, leApplies numeric bounds to the input.validation
reference_limitsUses the limits of another input as a validation reference.device-related bounds
alternative_groupAssociates inputs that represent alternative ways to express a similar choice.advanced GUI behavior
+ +## Units and Reference Units + +Two patterns are especially common. + +Explicit units: + +- `Annotated[float, ScanArgument(units=Units.s)]` +- `Annotated[float, ScanArgument(units=Units.eV)]` +- `Annotated[float, ScanArgument(units=Units.deg)]` + +Reference units: + +- `Annotated[float, ScanArgument(reference_units="motor1")]` +- `Annotated[float, ScanArgument(reference_units="motor2")]` +- `Annotated[float, ScanArgument(reference_units="device")]` + +Reference units are especially useful for scan inputs such as start, stop, or step size, where the +value should automatically use the same unit as a related device input. + +## How It Fits Into Scan Definitions + +`ScanArgument` does not replace the scan signature. It enriches it. + +The usual pattern is: + +1. define the Python parameter in `__init__` +2. wrap it in `Annotated[..., ScanArgument(...)]` +3. let BEC serialize that richer input definition for validation and GUI generation + +This is why `ScanArgument` shows up so often in real scan definitions: it is the bridge between the +scan's code-level inputs and its user-facing definition. + +## Next Step + +After `ScanArgument`, continue with [scan definition info](scan-definition-info.md), which covers +the broader scan-definition model around the signature, grouped positional inputs, and `gui_config`. + +## What To Remember + +!!! info "What to remember" + - `ScanArgument` attaches rich metadata to individual scan inputs. + - It is usually used through `Annotated[..., ScanArgument(...)]`. + - It helps BEC validate inputs and present them clearly in GUIs and clients. diff --git a/zensical.toml b/zensical.toml index 93e940f..336629a 100644 --- a/zensical.toml +++ b/zensical.toml @@ -125,8 +125,17 @@ nav = [ { "Simulated Devices" = "learn/devices/simulated-devices.md" }, ] }, ] }, - #{ "Scans and execution" = [ TODO fill with content - #] }, + { "Scans" = [ + { "Introduction to Scans" = "learn/scans/introduction.md" }, + { "Learn by Example" = "learn/scans/learn-by-example.md" }, + { "Scan Info" = "learn/scans/scan-info.md" }, + { "Scan Actions" = "learn/scans/scan-actions.md" }, + { "Scan Components" = "learn/scans/scan-components.md" }, + { "Position Generators" = "learn/scans/position-generators.md" }, + { "Fast Axis and Slow Axis" = "learn/scans/fast-axis-slow-axis.md" }, + { "ScanArgument" = "learn/scans/scanargument.md" }, + { "Scan Definition Info" = "learn/scans/scan-definition-info.md" }, + ] }, { "File Writing" = [ { "Introduction" = "learn/file-writer/introduction.md" }, { "Where Files Are Written" = "learn/file-writer/where-files-are-written.md" }, From aeb0298647670921959de73d873570b41898127f Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 6 May 2026 16:57:43 +0200 Subject: [PATCH 2/6] f --- docs/learn/scans/introduction.md | 101 ++++---------- docs/learn/scans/learn-by-example.md | 195 +++++++++++++++++---------- docs/learn/scans/lifecycle.md | 96 +++++++++++++ zensical.toml | 1 + 4 files changed, 251 insertions(+), 142 deletions(-) create mode 100644 docs/learn/scans/lifecycle.md diff --git a/docs/learn/scans/introduction.md b/docs/learn/scans/introduction.md index 581966e..5a3b348 100644 --- a/docs/learn/scans/introduction.md +++ b/docs/learn/scans/introduction.md @@ -11,7 +11,9 @@ related: # Scans in BEC !!! Info "Overview" - Scans are the core of BEC's functionality. They are the tools that move your devices, trigger readouts, and produce the data you analyze. In BEC, all scans follow a shared structure and report themselves in a consistent way, even when their motion logic differs. + Scans are the core of BEC's functionality. They are the tools that move your devices, trigger readouts, and produce the data you analyze. + + In BEC, all scans follow the same structure and report themselves in a consistent way, even when their motion logic differs. BEC scans follow one shared model. Whether you run a simple acquisition, a line scan, a grid scan, or a continuous scan, BEC handles them in the same overall way. @@ -39,83 +41,38 @@ For example, a line scan, a grid scan, and a continuous scan may move differentl At a high level, a scan in BEC follows this path: 1. The scan server publishes the available scan classes together with their serialized signatures, grouped inputs, and GUI metadata. -2. The client exposes those scans dynamically, so commands such as `scans.line_scan(...)` use the current server-side definition. -3. When a scan is called, the client validates the user inputs, resolves device-name strings to device objects where needed, and bundles repeated positional inputs for scans such as `line_scan` or `umv` before sending the request to the server. -4. On the server, the request is queued and receives runtime identifiers. -5. Once it is the scan's turn to run, the scan server queue hands over the request to a scan worker. -6. The scan worker runs the scan class's lifecycle hooks and sends device instructions through Redis. -7. Devices publish readouts and status updates. -8. The scan bundler groups those readouts into logical scan points. -9. Clients, history, and the file writer consume the resulting scan data. - -From the user side, the important part is consistency: every scan goes through the same backend -steps, reports its progress in the same general way, and produces scan data that can be accessed -through the same tools afterward. +1. The client exposes those scans dynamically, so commands such as `scans.line_scan(...)` use the current server-side definition. +1. When a scan is called, the client sends a request to the server with the scan class's name and the provided arguments. +1. On the server, the scan is assembled from the scan class and the provided arguments, and then it is put in the scan server queue. +1. Once it is the scan's turn to run, the scan server queue hands over the request to a scan worker. +1. The scan worker runs the scan class's lifecycle hooks. +1. During the scan, the scan class may use scan actions or components to trigger readouts, move devices, or run custom logic at each scan point. +1. Devices publish readouts and status updates. +1. The scan bundler groups those readouts into logical scan points. +1. Clients, history, and the file writer consume the resulting scan data. + +From the user side, the important part is consistency: every scan goes through the same lifecycle +steps. **If you have seen one scan, you have seen them all.** You can focus on the differences in motion logic without having to learn a new overall structure for each scan type. ## The Shared Scan Shape -As mentioned above, all BEC scans follow the same overall shape. They share the same lifecycle steps, and they use the same helpers to report themselves and produce data. This allows users to be always prompted with a familiar structure, even when the motion logic of the scan differs. The shared shape also makes it easier to learn new scans, since you can focus on the differences in motion logic rather than having to learn a new overall structure for each scan. The lifecycle steps are: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HookRole
prepare_scanPrepare the scan for the upcoming acquisition.
open_scanOpen the scan and emit a new scan status message with all relevant metadata.
stageStage the devices for the upcoming acquisition.
pre_scanRun any pre-scan logic, such as preparing time-critical devices.
scan_coreRun the core logic of the scan; trigger readouts if needed and optionally call at_each_point() for per-point logic.
post_scanRun any post-scan logic, such as moving devices back to their original position.
unstageUnstage the devices.
close_scanClose the scan and emit a final scan status message with all relevant metadata.
on_exceptionIf an exception is raised during any earlier step, run cleanup logic here.
- -BEC runs these lifecycle steps in the same order every time. - -This shared shape is why BEC scans feel related rather than like separate one-off tools. +As mentioned above, all BEC scans follow the same overall shape. They use the same lifecycle and +the same helpers to report themselves and produce data. That shared structure is why BEC scans feel +related rather than like separate one-off tools. + +The details of a scan may change a lot from one scan type to another, but the lifecycle around +those details stays recognizable. That makes it easier to learn new scans because you can focus on +the motion logic instead of relearning the full structure each time. + +The next page, [Scan Lifecycle](lifecycle.md), breaks down those shared lifecycle steps and what +each hook is responsible for. ## Where To Go Next If you want the next layer of detail: +- read [Scan Lifecycle](lifecycle.md){ data-preview } to see the shared hook order used by every + BEC scan. - read [Learn by example](../learn/scans/learn-by-example.md){ data-preview } to go through the `acquire` scan example in detail, and see how the shared scan shape applies to a specific scan type. ## What to Remember @@ -124,5 +81,5 @@ If you want the next layer of detail: - In BEC, all scans follow the same overall shape. - Different scan types use the same backend framework, even when their motion logic differs. - Every scan reports itself in a common structured way while it runs. - - Every scan has to implement the same lifecycle steps, even when some of those steps are empty. - - The lifecycle steps are called in the same order for every scan. + - Every scan follows the same lifecycle, even when some hooks do very little. + - The lifecycle order is fixed, which makes new scan types easier to understand. diff --git a/docs/learn/scans/learn-by-example.md b/docs/learn/scans/learn-by-example.md index c3ffeed..d534144 100644 --- a/docs/learn/scans/learn-by-example.md +++ b/docs/learn/scans/learn-by-example.md @@ -31,7 +31,7 @@ trajectory logic on top. It still demonstrates the main parts of a real scan: -- a compact `__init__` with `ScanArgument` metadata +- a compact `__init__` with shared default argument types - `gui_config` for graphical clients - a small but meaningful `prepare_scan` - a short `scan_core` @@ -45,174 +45,229 @@ what inputs it accepts. ```py from __future__ import annotations -from typing import Annotated +import numpy as np -from bec_lib.scan_args import ScanArgument, Units -from bec_server.scan_server.scans import ScanBase, ScanType +from bec_lib.scan_args import DefaultArgType +from bec_server.scan_server.scans.scan_base import ScanBase, ScanType from bec_server.scan_server.scans.scan_modifier import scan_hook class Acquire(ScanBase): scan_type = ScanType.SOFTWARE_TRIGGERED # (1)! - scan_name = "acquire" # (2)! + scan_name = "_v4_acquire" # (2)! gui_config = { # (3)! - "Acquisition Parameters": [ + "Scan Parameters": [ "exp_time", "frames_per_trigger", "settling_time", + "settling_time_after_trigger", "readout_time", - ], + "burst_at_each_point", + ] } def __init__( - self, - exp_time: Annotated[ - float, ScanArgument(display_name="Exposure Time", units=Units.s, ge=0) - ] = 0, # (4)! - frames_per_trigger: Annotated[ - int, ScanArgument(display_name="Frames per Trigger", ge=1) - ] = 1, - settling_time: Annotated[ - float, ScanArgument(display_name="Settling Time", units=Units.s, ge=0) - ] = 0, - readout_time: Annotated[ - float, ScanArgument(display_name="Readout Time", units=Units.s, ge=0) - ] = 0, + self, # (4)! + exp_time: DefaultArgType.ExposureTime = 0, + frames_per_trigger: DefaultArgType.FramesPerTrigger = 1, + settling_time: DefaultArgType.SettlingTime = 0, + settling_time_after_trigger: DefaultArgType.SettlingTimeAfterTrigger = 0, + readout_time: DefaultArgType.ReadoutTime = 0, + burst_at_each_point: DefaultArgType.BurstAtEachPoint = 1, **kwargs, # (5)! ): super().__init__(**kwargs) # (6)! + self.motors = [] self.exp_time = exp_time self.frames_per_trigger = frames_per_trigger self.settling_time = settling_time + self.settling_time_after_trigger = settling_time_after_trigger self.readout_time = readout_time + self.burst_at_each_point = burst_at_each_point self.update_scan_info( # (7)! exp_time=exp_time, frames_per_trigger=frames_per_trigger, settling_time=settling_time, + settling_time_after_trigger=settling_time_after_trigger, readout_time=readout_time, + burst_at_each_point=burst_at_each_point, ) ``` 1. `scan_type` tells BEC that this scan's main logic is software-triggered. 2. `scan_name` is the name published by the scan server and exposed to the client. It must be unique across all scans and a valid Python identifier. Once loaded, the scan is available as `scans.(...)` on the client. -3. The `gui_config` dictionary groups and enables input fields in graphical clients. In this case, all inputs are grouped under "Acquisition Parameters". Keys that don't appear in `gui_config` are still valid inputs from the command-line but they won't be shown in GUIs. -4. `ScanArgument(...)` carries labels, units, and validation bounds. In this case, the `exp_time` argument must be a float and the `ScanArgument` further specifies that it should be displayed as "Exposure Time" with units of seconds and the value must be greater than or equal to (ge) 0. +3. The `gui_config` dictionary groups and enables input fields in graphical clients. In the current implementation, `acquire` groups its inputs under "Scan Parameters" and includes timing plus burst settings. Keys that don't appear in `gui_config` are still valid inputs from the command-line but they won't be shown in GUIs. +4. The current scan uses shared `DefaultArgType` aliases instead of spelling out `Annotated[..., ScanArgument(...)]` for each input. That keeps the signature compact while still reusing the standard scan argument definitions for exposure time, settling, readout, and burst settings. 5. `**kwargs` is needed to be able to make the scan connect to BEC's scan lifecycle and forward additional metadata to `ScanBase`. -6. The `super().__init__(**kwargs)` call is required to properly initialize the scan base class and give the scan access to devices, actions, components, and the scan info object. It usually doesn't need to receive any additional arguments, only `**kwargs`. -7. Use `update_scan_info(...)` to update any standard scan metadata field that depends on the scan inputs. This is important as it is the scan info object that is used for broadcasting information about the scan to clients, devices, and the file writer. In this case, the acquisition parameters are added to the scan info for later reference. +6. The `super().__init__(**kwargs)` call is required to properly initialize the scan base class and give the scan access to devices, actions, components, and the scan info object. In this scan, `self.motors = []` also makes it explicit that `acquire` does not move any motors. +7. Use `update_scan_info(...)` to update any standard scan metadata field that depends on the scan inputs. This is important as it is the scan info object that is used for broadcasting information about the scan to clients, devices, and the file writer. In this case, the acquisition parameters, including post-trigger settling and burst count, are added to the scan info for later reference. -## `prepare_scan`: Small But Still Important +## `prepare_scan` -Even a short scan usually uses `prepare_scan` to finalize metadata and publish progress reporting. +For `acquire`, we only measure at the current position, so `prepare_scan` does not need to generate a list of positions or check motor limits. We update the scan info container with the acquisition parameters: no positions, one logical point and depending on the `burst_at_each_point` setting, one or more monitored readouts. ```py + @scan_hook def prepare_scan(self): - self.update_scan_info( # (1)! + self.update_scan_info( + positions=np.array([]), num_points=1, - num_monitored_readouts=self.frames_per_trigger, + num_monitored_readouts=self.burst_at_each_point, ) - self.actions.add_scan_report_instruction_scan_progress( # (2)! + self.actions.add_scan_report_instruction_scan_progress( points=self.scan_info.num_monitored_readouts, show_table=False, ) - self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) # (3)! + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) ``` -1. The scan declares its final runtime shape: one acquisition step with a known readout count. -2. The progress instruction lets clients render consistent progress even for a simple scan. -3. Baseline readout starts early and can finish in parallel with the rest of scan setup. +Once the scan info is updated with the correct parameters, we can send the information about the upcoming acquisitions to any clients that may want to report on the scan progress. In this case, we use `add_scan_report_instruction_scan_progress` to suggest that clients report progress based on the number of monitored readouts, which is the most relevant measure of progress for this scan. The `show_table=False` argument indicates that clients should not show a progress table with individual point statuses, just a progress bar. -This is useful to compare with a motor scan: the hook is still important, but it is shorter because -there are no positions to generate, shift, or limit-check. +Finally, `prepare_scan` triggers a readout of all devices of readout priority `baseline`. This is done asynchronously by passing `wait=False`, so the scan can proceed to `open_scan` while the baseline readout is still in progress. The resulting status object is stored in `self._baseline_readout_status` so that we can check on it later (in `close_scan`) and make sure the baseline readout has finished before closing the scan. -## The Shared Lifecycle Around The Acquisition +## `open_scan` -After `prepare_scan`, the scan looks much like any other BEC scan again. +After `prepare_scan`, `acquire` opens the scan in the standard way. ```py @scan_hook def open_scan(self): self.actions.open_scan() +``` + +Opening the scan will emit a new scan status message with all the metadata we prepared in scan info, so it is important to make sure the scan info is up to date before this step. + +Any device that implements runtime logic based on the scan info metadata will now receive the updated scan info. + +## `stage` + +Stageing tells devices to get ready for the upcoming acquisition. +```py @scan_hook def stage(self): self.actions.stage_all_devices() +``` + +If a device implements custom `on_stage` logic, it will be triggered by `stage_all_devices()`. For example, a detector may configure itself for acquisition during staging, and then be ready to receive trigger signals once the scan starts. + +## `pre_scan` + +`pre_scan` gives devices one last chance to prepare before the acquisition starts. +```py @scan_hook def pre_scan(self): - self.actions.pre_scan_all_devices() # (1)! + self.actions.pre_scan_all_devices() +``` + +In `acquire`, we don't have any time-critical motors to prepare, so we can just delegate to the shared `pre_scan_all_devices` helper. In a motor-driven scan, this is where we would typically wait for the motors to reach their starting positions before triggering any `pre_scan` logic. + +In general, `pre_scan` is meant for any preparation that needs to happen after the scan is open but before the first acquisition starts. It is a good place for time-critical preparation, e.g. devices that have a very short window between being armed and needing to receive a first trigger. + +## `scan_core` +`scan_core` contains the main acquisition loop. + +```py @scan_hook def scan_core(self): - for _ in range(self.frames_per_trigger): # (2)! - self.components.trigger_and_read() # (3)! + for _ in range(self.burst_at_each_point): + self.at_each_point() ``` -1. `pre_scan_all_devices()` runs the usual last-moment device preparation. -2. `frames_per_trigger` still affects the acquisition loop, even though nothing is moving. -3. The scan reuses the shared trigger-and-read helper instead of implementing detector logic itself. +As we are not moving between acquisitions in this scan, the core loop is merely a burst loop that calls `at_each_point` for each acquisition. The `at_each_point` hook is a common extension point for scans that have a repetitive acquisition step, such as line scans, grid scans, or in this case, a burst of acquisitions at each point. By putting the trigger-and-read logic in `at_each_point`, we can keep the main loop in `scan_core` clean and focused on the overall structure of the scan, while still allowing for complex per-point logic when needed. -This is the key lesson of the `acquire` example: even the shortest useful scan still fits the same -hook order and still delegates repetitive device work to shared helpers. +## `at_each_point` -## Cleanup +The actual trigger-and-read work happens in a separate per-point hook. -The final hooks show how the scan finishes cleanly. +```py +@scan_hook +def at_each_point(self): + self.components.trigger_and_read() +``` + +The `trigger_and_read` component is a shared helper that triggers all devices that are set to `softwareTrigger=True` before starting a readout of all devices of readout priority `monitored`. + +## `post_scan` + +Once acquisition is done, `post_scan` lets devices finish their scan-side work. ```py @scan_hook def post_scan(self): - self.actions.complete_all_devices() # (1)! + self.actions.complete_all_devices() +``` + +The `complete_all_devices` helper calls the `complete` method on all devices that implement it. The `complete` method on a device is meant for any logic that needs to happen after the last acquisition has been triggered but before the scan is closed. For example, a detector may need to wait until all frames have been read out and processed before it can report that it is done with the scan. +## `unstage` + +After that, the scan unstages devices in the standard way. + +```py @scan_hook def unstage(self): self.actions.unstage_all_devices() +``` + +## `close_scan` +The closing hook waits for the baseline readout to finish before emitting the final scan status. + +```py @scan_hook def close_scan(self): - if self._baseline_readout_status is not None: # (2)! + if self._baseline_readout_status is not None: self._baseline_readout_status.wait() self.actions.close_scan() - self.actions.check_for_unchecked_statuses() # (3)! + self.actions.check_for_unchecked_statuses() +``` + +An additional check is performed to make sure that no status objects were left unchecked at the end of the scan. If any status was left unchecked, a warning will be logged. + +## `on_exception` + +If any exception is raised during the scan lifecycle, the `on_exception` hook gives the scan a chance to clean up. + +```py +@scan_hook +def on_exception(self, exception: Exception): + pass ``` -1. `post_scan` lets devices finish their acquisition-side work before teardown. -2. `close_scan` waits for the asynchronous baseline readout to finish. -3. The final status check helps catch unfinished device operations. +In this case, there is no special cleanup needed. -This cleanup path is shorter than in a relative motor scan, because there are no motors to move -back and no point-pattern state to unwind. ## What This Example Shows The `acquire` scan is a compact example of the shared scan shape: -- the hook order is unchanged -- `prepare_scan` is still where final scan metadata is prepared -- `scan_core` stays short because it composes shared helpers -- scan-definition metadata such as `gui_config` and `ScanArgument` lives alongside the code -- even a simple scan still needs explicit cleanup +- the same lifecycle from [Scan Lifecycle](lifecycle.md) appears here unchanged +- each hook stays small and focused, even though the page now looks at them one by one +- `prepare_scan` and `scan_core` are the most informative hooks for understanding what this scan actually does +- `at_each_point` shows how per-acquisition work can be factored out of the main loop +- the rest of the hooks mostly delegate to shared helpers in `actions` and `components` +- scan-definition metadata such as `gui_config` and the shared default argument types lives alongside the executable scan code ## Next Step -After this example, the next useful topic is [scan info](scan-info.md). - -That page explains the shared metadata object this scan keeps updating through -`update_scan_info(...)`. +After this example, the next useful topic is [scan info](scan-info.md), because `acquire` +updates `scan_info` in `__init__` and `prepare_scan` and then relies on that shared runtime +metadata for progress reporting and scan status messages. -After `scan info`, continue with [scan actions](scan-actions.md), and then move on to -[scan components](scan-components.md) for `components`. +After `scan info`, continue with [scan actions](scan-actions.md) to see the building blocks used in scans for common operations. Afterwards, [scan components](scan-components.md) show how to combine scan actions into reusable scan building blocks. ## What to Remember !!! info "What to remember" - - This page uses `acquire` because it shows the shared scan shape with minimal extra logic. - - The scan-specific parts are mostly in `__init__`, `prepare_scan`, and the short acquisition loop in `scan_core`. - - The repetitive execution path is delegated to shared helpers in `actions` and `components`. - - More complex scans keep the same lifecycle, but add motion planning and richer cleanup on top. + - This page uses `acquire` because it shows the full lifecycle with almost no motion-planning noise. + - In `acquire`, most of the scan-specific behavior lives in `__init__`, `prepare_scan`, `scan_core`, and `at_each_point`. + - More complex scans keep the same lifecycle, but make hooks such as `prepare_scan`, `scan_core`, `post_scan`, and `on_exception` richer. diff --git a/docs/learn/scans/lifecycle.md b/docs/learn/scans/lifecycle.md new file mode 100644 index 0000000..3e3246d --- /dev/null +++ b/docs/learn/scans/lifecycle.md @@ -0,0 +1,96 @@ +--- +related: + - title: Introduction to Scans + url: learn/scans/introduction.md + - title: Learn by Example + url: learn/scans/learn-by-example.md + - title: Scan Info + url: learn/scans/scan-info.md +--- + +# Scan Lifecycle + +BEC uses one shared scan structure across the system. Concrete scans such as `line_scan`, +`grid_scan`, `time_scan`, or `monitor_scan` all follow that same structure, even if they move +different devices or collect data in different ways. + +Scans implement these hooks as normal methods and the scan server calls those hooks in a fixed order, which gives every BEC scan its recognizable shape. + + +## The Fixed Hook Order + +Scans are structured around the following hooks: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HookRole
prepare_scanPrepare the scan for the upcoming acquisition.
open_scanOpen the scan and emit a new scan status message with all relevant metadata.
stageStage the devices for the upcoming acquisition.
pre_scanRun any pre-scan logic, such as preparing time-critical devices.
scan_coreRun the core logic of the scan; trigger readouts if needed and optionally call at_each_point() for per-point logic.
post_scanRun any post-scan logic, such as moving devices back to their original position.
unstageUnstage the devices.
close_scanClose the scan and emit a final scan status message with all relevant metadata.
on_exceptionIf an exception is raised during any earlier step, run cleanup logic here.
+ + +???+ Example "Example hook order in `fermat_spiral`" + As an example, the `fermat_spiral` scan implements the lifecycle hooks in the following way: + + - `prepare_scan`: Build the Fermat position list, optionally shift it by the current motor positions for relative scans, check motor limits, update `scan_info`, schedule scan-progress reporting, trigger a baseline readout, and start moving to the first point. + - `open_scan`: Open a new scan by calling `self.actions.open_scan()` after the metadata is ready. + - `stage`: Stage all participating devices through `self.actions.stage_all_devices()`. + - `pre_scan`: Wait until the motors have reached the first point, then run `pre_scan` on all devices. + - `scan_core`: Run a step scan over the prepared spiral positions and call `at_each_point` so each point performs the move/trigger/readout sequence. + - `post_scan`: Ask all devices to complete their work and, for relative scans, move the motors back to their original positions. + - `unstage`: Unstage all devices through `self.actions.unstage_all_devices()`. + - `close_scan`: Wait for the asynchronous baseline readout to finish, publish the closing scan status, and check that no statuses were left unchecked. + - `on_exception`: If the scan was relative, move the motors back to their recorded starting positions. + + +## What to Remember + +!!! info "What to remember" + - Every BEC scan follows the same lifecycle, even when the acquisition details differ. + - The hook order is fixed. + - The lifecycle stays recognizable even when the details of a scan change. diff --git a/zensical.toml b/zensical.toml index 336629a..f281432 100644 --- a/zensical.toml +++ b/zensical.toml @@ -127,6 +127,7 @@ nav = [ ] }, { "Scans" = [ { "Introduction to Scans" = "learn/scans/introduction.md" }, + { "Scan Lifecycle" = "learn/scans/lifecycle.md" }, { "Learn by Example" = "learn/scans/learn-by-example.md" }, { "Scan Info" = "learn/scans/scan-info.md" }, { "Scan Actions" = "learn/scans/scan-actions.md" }, From 9aeb0745068983961b9dbceda52d11dcdf533086 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 6 May 2026 17:08:10 +0200 Subject: [PATCH 3/6] f --- docs/learn/scans/learn-by-example.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/learn/scans/learn-by-example.md b/docs/learn/scans/learn-by-example.md index d534144..53de6b9 100644 --- a/docs/learn/scans/learn-by-example.md +++ b/docs/learn/scans/learn-by-example.md @@ -54,7 +54,7 @@ from bec_server.scan_server.scans.scan_modifier import scan_hook class Acquire(ScanBase): scan_type = ScanType.SOFTWARE_TRIGGERED # (1)! - scan_name = "_v4_acquire" # (2)! + scan_name = "acquire" # (2)! gui_config = { # (3)! "Scan Parameters": [ @@ -250,7 +250,7 @@ In this case, there is no special cleanup needed. The `acquire` scan is a compact example of the shared scan shape: -- the same lifecycle from [Scan Lifecycle](lifecycle.md) appears here unchanged +- the same lifecycle from [Scan Lifecycle](lifecycle.md){data-preview} appears here unchanged - each hook stays small and focused, even though the page now looks at them one by one - `prepare_scan` and `scan_core` are the most informative hooks for understanding what this scan actually does - `at_each_point` shows how per-acquisition work can be factored out of the main loop @@ -259,11 +259,11 @@ The `acquire` scan is a compact example of the shared scan shape: ## Next Step -After this example, the next useful topic is [scan info](scan-info.md), because `acquire` +After this example, the next useful topic is [scan info](scan-info.md){data-preview}, because `acquire` updates `scan_info` in `__init__` and `prepare_scan` and then relies on that shared runtime metadata for progress reporting and scan status messages. -After `scan info`, continue with [scan actions](scan-actions.md) to see the building blocks used in scans for common operations. Afterwards, [scan components](scan-components.md) show how to combine scan actions into reusable scan building blocks. +After `scan info`, continue with [scan actions](scan-actions.md){data-preview} to see the building blocks used in scans for common operations. Afterwards, [scan components](scan-components.md){data-preview} show how to combine scan actions into reusable scan building blocks. ## What to Remember From 885b30daefff334f84099c8be1c3412723ac9901 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 6 May 2026 19:22:48 +0200 Subject: [PATCH 4/6] f --- docs/learn/scans/argument-bundles.md | 88 ++++++++ docs/learn/scans/gui-config.md | 78 +++++++ docs/learn/scans/learn-by-example.md | 4 +- docs/learn/scans/motions.md | 69 +++++++ docs/learn/scans/scan-actions.md | 193 +----------------- docs/learn/scans/scan-definition-info.md | 129 ------------ docs/learn/scans/scan-info.md | 76 +------ docs/learn/scans/scanargument.md | 83 ++++---- .../bec-core/scan-actions-methods.md | 160 +++++++++++++++ zensical.toml | 39 ++-- 10 files changed, 474 insertions(+), 445 deletions(-) create mode 100644 docs/learn/scans/argument-bundles.md create mode 100644 docs/learn/scans/gui-config.md create mode 100644 docs/learn/scans/motions.md delete mode 100644 docs/learn/scans/scan-definition-info.md create mode 100644 docs/references/bec-core/scan-actions-methods.md diff --git a/docs/learn/scans/argument-bundles.md b/docs/learn/scans/argument-bundles.md new file mode 100644 index 0000000..3b5df9e --- /dev/null +++ b/docs/learn/scans/argument-bundles.md @@ -0,0 +1,88 @@ +--- +related: + - title: ScanArgument + url: learn/scans/scanargument.md + - title: GUI Config + url: learn/scans/gui-config.md + - title: Learn by Example + url: learn/scans/learn-by-example.md +--- + +# Argument Bundles + +This page explains how scans can describe repeated positional input bundles when a normal fixed +Python signature is not enough. + +## The Scan Signature + +In the current scan implementation, a scan definition is described largely by its `__init__` +signature plus a few class attributes. + +The scan server serializes that signature and publishes it to clients. The client then uses it to: + +- expose the scan under `scans.` +- attach a live Python signature in IPython +- validate kwargs and bundled positional inputs +- resolve device-name strings to device objects when the annotations require that + +This means the signature is no longer only local Python documentation. It is part of the runtime API +contract between the scan server, the client, and GUIs. + +## `arg_input` and `arg_bundle_size` + +!!! tip + `arg_input` and `arg_bundle_size` are special cases for scans with an undefined number of + input arguments. Most scans developed in plugins do not need them. + +If the number of input arguments is not fixed, the usual Python-style fixed signature is not enough. + +For example, a line scan can work with any number of motors in parallel, so the scan cannot rely on +one fixed positional argument layout in the way an ordinary Python function usually would. + +In those cases, scans with repeated positional bundles declare those bundles explicitly. + +For a line scan, that can look like this: + +```py +arg_input = { + "device": DeviceBase, + "start": float, + "stop": float, +} +arg_bundle_size = {"bundle": 3, "min": 1, "max": None} +``` + +`arg_bundle_size` then tells BEC how many positional values belong to one bundle and how many +bundles are allowed. + +This is what lets BEC validate a call such as: + +```py +scans.line_scan(dev.samx, -1, 1, dev.samy, -2, 2, steps=5, relative=False) +``` + +without treating those positional arguments as an unstructured `*args` blob. + +Rich input metadata for individual parameters is covered separately on +[ScanArgument](scanargument.md). + +## Reloading The Scan Server + +If you add a new scan or change an existing scan class, the scan server must reload that Python code +before the changes become available. + +In practice, that means you should restart or reload the scan server after editing scan +implementations. Otherwise the running server will continue using the old version of the scan. + +## Next Step + +After argument bundles, continue with [GUI Config](gui-config.md) to see how scans group inputs for +graphical clients. + +## What To Remember + +!!! info "What to remember" + - The serialized scan signature is part of the runtime API between the scan server, clients, and GUIs. + - `arg_input` and `arg_bundle_size` define repeated positional bundles such as move targets or line-scan ranges. + - `ScanArgument` covers rich metadata for individual inputs, while argument bundles describe repeated positional structure. + - After changing scan code, the scan server must be reloaded or restarted. diff --git a/docs/learn/scans/gui-config.md b/docs/learn/scans/gui-config.md new file mode 100644 index 0000000..228426c --- /dev/null +++ b/docs/learn/scans/gui-config.md @@ -0,0 +1,78 @@ +--- +related: + - title: ScanArgument + url: learn/scans/scanargument.md + - title: Argument Bundles + url: learn/scans/argument-bundles.md + - title: Learn by Example + url: learn/scans/learn-by-example.md +--- + +# GUI Config + +This page explains how scans can group their inputs for graphical clients through `gui_config`. + +## What `gui_config` Does + +`gui_config` describes how scan inputs should be grouped in graphical interfaces. + +For example, a scan can group inputs under headings such as: + +- `Device` +- `Movement Parameters` +- `Acquisition Parameters` + +This does not change how the scan runs. It helps GUIs present scan inputs in a clear structure +instead of showing one flat list of parameters. + +## A Typical Example + +```py +gui_config = { + "Device 1": ["motor1", "start_motor1", "stop_motor1"], + "Device 2": ["motor2", "start_motor2", "stop_motor2"], + "Movement Parameters": ["step", "relative"], + "Acquisition Parameters": [ + "exp_time", + "frames_per_trigger", + "settling_time", + "readout_time", + ], +} +``` + +In this example, the scan definition is still the same Python class and the same Python signature. +`gui_config` only changes how that information is grouped and presented in graphical clients. + +## How It Fits With Scan Signatures + +`gui_config` does not replace the scan signature or `ScanArgument` metadata. + +Instead, these pieces work together: + +- the signature defines which inputs exist +- `ScanArgument` enriches individual inputs with labels, units, bounds, and descriptions +- `gui_config` groups those inputs into a clearer layout for GUIs + +This is why `gui_config` is best thought of as presentation metadata rather than execution logic. + +## Reloading The Scan Server + +If you add a new scan or change an existing scan class, the scan server must reload that Python code +before the changes become available. + +In practice, that means you should restart or reload the scan server after editing scan +implementations. Otherwise the running server will continue using the old version of the scan. + +## Next Step + +If you want to see `gui_config` and related scan-definition details in a richer real scan, read the +[worked example: hexagonal scan](hexagonal-scan-example.md). + +## What To Remember + +!!! info "What to remember" + - `gui_config` groups scan inputs for graphical clients. + - It changes presentation, not scan execution. + - `gui_config` works alongside the scan signature and `ScanArgument` metadata. + - After changing scan code, the scan server must be reloaded or restarted. diff --git a/docs/learn/scans/learn-by-example.md b/docs/learn/scans/learn-by-example.md index 53de6b9..1dda7b8 100644 --- a/docs/learn/scans/learn-by-example.md +++ b/docs/learn/scans/learn-by-example.md @@ -12,8 +12,8 @@ related: url: learn/scans/scan-info.md - title: ScanArgument url: learn/scans/scanargument.md - - title: Scan Definition Info - url: learn/scans/scan-definition-info.md + - title: GUI Config + url: learn/scans/gui-config.md --- # Learn by Example diff --git a/docs/learn/scans/motions.md b/docs/learn/scans/motions.md new file mode 100644 index 0000000..3640b6c --- /dev/null +++ b/docs/learn/scans/motions.md @@ -0,0 +1,69 @@ +--- +related: + - title: Scan Lifecycle + url: learn/scans/lifecycle.md + - title: Scan Components + url: learn/scans/scan-components.md + - title: Argument Bundles + url: learn/scans/argument-bundles.md +--- + +# Motions + +In BEC, a scan does not have to acquire detector data. + +A coordinated motion can also be treated as a scan if it uses the same lifecycle, reporting model, +and request interface as other scans. That is why commands such as `mv` and `umv` are implemented +with the same scan framework even though their main job is to reposition motors. + +This allows motion-only commands to benefit from the scan infrastructure: + +- it can reuse the same argument handling and validation +- it can publish status in the same general format +- it can use the same actions and components helpers +- it can fit naturally into the same client and server model as other scan-like operations + +## Marking A Motion-Only Command + +There are two flags that a scan can set to indicate that it is not a data-taking scan: +- `is_scan=False` indicates that the operation is not a scan, so it should be kept separate from ordinary scan entries in user interfaces. +- `scan_type=None` indicates that the operation is neither hardware-triggered nor software-triggered, so it should not be confused with acquisition scans. + +## `move` and `updated_move` + +Both `move` and `updated_move` are motion commands implemented through the scan interface. + +They accept repeated motor/target bundles, support relative motion, and run through the same hook +structure as other scans. + +They are exposed as `scans.mv` and `scans.umv` or through the high-level-interface as + +- `umv` for `scans.umv(..., relative=False)` +- `umvr` for `scans.umv(..., relative=True)` +- `mv` for `scans.mv(..., relative=False)` +- `mvr` for `scans.mv(..., relative=True)` + +## The Shared Lifecycle + +Even though this command is motion-only, it still defines the same lifecycle hooks: + +- `prepare_scan` +- `open_scan` +- `stage` +- `pre_scan` +- `scan_core` +- `post_scan` +- `unstage` +- `close_scan` +- `on_exception` + +In motion-only scans, most lifecycle hooks are kept empty or very simple. In particular, `scan_core` is the main place where the motion logic lives. + +## What To Remember + +!!! info "What to remember" + - In BEC, a scan does not have to acquire data to use the scan framework. + - Coordinated motions such as `move` and `updated_move` can use the same lifecycle and reporting model as scans. + - Motion-only commands can still reuse argument bundles, actions, components, and scan status reporting. + - To mark a motion-only command, set `is_scan=False` and `scan_type=None` in the scan class. + - Lifecycle hooks must be defined, but they can be kept simple or empty if they are not needed. diff --git a/docs/learn/scans/scan-actions.md b/docs/learn/scans/scan-actions.md index 6fda40a..b8c5c22 100644 --- a/docs/learn/scans/scan-actions.md +++ b/docs/learn/scans/scan-actions.md @@ -10,199 +10,26 @@ related: # Scan Actions -`self.actions` is the high-level interface a scan uses to interact with devices and publish -scan-related state. +Scan Actions are the building blocks for scan work. They are the main way a concrete scan performs operations such as opening, staging, triggering, reading, and closing a scan. They are also the main way a concrete scan updates its runtime metadata and published status. Every scan has access to these operations through `self.actions` from any scan hook or method, and they are designed to be the preferred way for concrete scans to perform common work and updates. -In practice, `self.actions` is an instance of `ScanActions`. These methods -wrap common device-server instructions, track their statuses, and update scan-report state so that -concrete scans do not need to reimplement the same coordination logic again and again. +!!! Info "Scan Actions vs. Scan Components" + Scan Actions are the lower-level building blocks, while Scan Components are larger, more bespoke patterns built on top of Scan Actions. -## What `actions` Is For +## What Scan Actions Are For -`actions` is typically used for three kinds of work: +Scan Actions are typically used for three kinds of work: - lifecycle orchestration such as opening, staging, and closing a scan - device operations such as moving, triggering, reading, and completing - reporting and metadata updates such as progress instructions and readout-priority changes -The important design point is that `actions` stays scan-facing and task-oriented. A scan usually -asks for a high-level operation like `stage_all_devices()` or `read_baseline_devices()` rather than -building instruction messages manually. +## ScanActions Methods -## Common `ScanActions` Methods +For a full list of available methods, see the reference page for +[ScanActions Methods](../../references/bec-core/scan-actions-methods.md){ data-preview }. -The current public `ScanActions` API includes the following methods most relevant to scan authors. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodRoleTypical hook
open_scan()Publishes the opening scan status message for the current scan.open_scan
stage_all_devices()Stages all enabled scan devices, with async devices handled separately for better throughput.stage
stage(...)Stages one device or a selected device list instead of the whole scan device set.stage in custom cases
pre_scan_all_devices()Runs the pre-scan device step across all enabled devices.pre_scan
pre_scan(...)Runs the pre-scan device step only for selected devices.pre_scan in custom cases
set(...)Sends coordinated set instructions to one or several devices.prepare_scan or scan_core
kickoff(...)Starts a kickoff-capable device with optional configuration parameters.scan_core or flyer setup
complete(...)Completes one device explicitly.post_scan
complete_all_devices()Completes all enabled scan devices.post_scan
read_monitored_devices()Reads the current monitored-device group and advances the monitored readout counter.scan_core
read_manually(...)Performs an explicit read and returns the result to the scan, rather than relying on the usual monitored-read path.Special cases only
publish_manual_read(...)Publishes externally collected data as the next monitored readout.Special cases only
read_baseline_devices()Reads the baseline-device group, often around scan setup or teardown.Usually prepare_scan
trigger_all_devices()Triggers all devices configured for software triggering in the scan.scan_core
unstage(...)Unstages one selected device.unstage in custom cases
unstage_all_devices()Unstages all enabled scan devices.unstage
add_scan_report_instruction_scan_progress(...)Adds a scan-progress instruction so clients can render scan progress consistently.Usually prepare_scan
add_scan_report_instruction_readback(...)Adds a live readback instruction for selected devices.Usually prepare_scan
add_scan_report_instruction_device_progress(...)Adds a device-progress instruction for devices exposing progress signals.Usually prepare_scan
set_device_readout_priority(...)Modifies which devices are treated as monitored, baseline, on-request, or async during the scan.Usually __init__ or prepare_scan
close_scan()Finalizes monitored-readout counts, checks cleanup state, and publishes the closing scan status.close_scan
check_for_unchecked_statuses()Warns about unfinished or unchecked status objects and waits on remaining work when needed.close_scan or cleanup
add_device_with_required_response(...)Marks devices whose instructions must emit explicit response messages.Special cases only
rpc_call(...)Makes a low-level RPC call to a device-server method and returns the result.Advanced cases only
send_client_info(...)Sends an informational message to clients, for example for GUI status updates.Any hook when useful
- -## How `actions` Fits With `scan_info` - -Many `actions` methods do more than send device instructions. - -For example: - -- `add_scan_report_instruction_*` updates `scan_info.scan_report_instructions` -- `set_device_readout_priority(...)` updates `scan_info.readout_priority_modification` -- `close_scan()` writes back the actual `num_monitored_readouts` before publishing the closing status - -So `actions` is one of the main ways a concrete scan both performs work and keeps its shared runtime -description synchronized. - -## When To Prefer `actions` - -Prefer `actions` when you want to express an operation in scan terms: - -- open or close the scan -- stage or unstage devices -- trigger or read the configured scan device groups -- publish scan progress or readback instructions -- adjust scan-local readout priorities - -This keeps scan code short and consistent. - -If you need reusable lower-level scan logic such as step-scan execution, motor limit checking, or -move-and-wait helpers, that belongs in `components`. +!!! Tip + We recommend browsing the reference page to get a sense of the available methods and their purposes but do not recommend trying to memorize them. When writing a concrete scan, you can always use auto-complete to find the right method for your needs. More importantly, we recommend going through actual scan implementations to see how these methods are used in practice. ## Next Step diff --git a/docs/learn/scans/scan-definition-info.md b/docs/learn/scans/scan-definition-info.md deleted file mode 100644 index b60c953..0000000 --- a/docs/learn/scans/scan-definition-info.md +++ /dev/null @@ -1,129 +0,0 @@ -# Scan Definition Info - -This page explains the extra information a scan class can define so that the rest of BEC can present -and understand the scan correctly. - -## The Scan Signature - -In the current scan implementation, a scan definition is described largely by its `__init__` -signature plus a few class attributes. - -The scan server serializes that signature and publishes it to clients. The client then uses it to: - -- expose the scan under `scans.` -- attach a live Python signature in IPython -- validate kwargs and bundled positional inputs -- resolve device-name strings to device objects when the annotations require that - -This means the signature is no longer only local Python documentation. It is part of the runtime API -contract between the scan server, the client, and GUIs. - -## `arg_input` and `arg_bundle_size` - -!!! tip - `arg_input` and `arg_bundle_size` are special cases for scans with an undefined number of - input arguments. Most scans developed in plugins do not need them. - -If the number of input arguments is not fixed, the usual Python-style fixed signature is not enough. - -For example, a line scan can work with any number of motors in parallel, so the scan cannot rely on -one fixed positional argument layout in the way an ordinary Python function usually would. - -In those cases, scans with repeated positional bundles declare those bundles explicitly. - -For a line scan, that can look like this: - -```py -arg_input = { - "device": DeviceBase, - "start": float, - "stop": float, -} -arg_bundle_size = {"bundle": 3, "min": 1, "max": None} -``` - -`arg_bundle_size` then tells BEC how many positional values belong to one bundle and how many -bundles are allowed. - -This is what lets BEC validate a call such as: - -```py -scans.line_scan(dev.samx, -1, 1, dev.samy, -2, 2, steps=5, relative=False) -``` - -without treating those positional arguments as an unstructured `*args` blob. - -Rich input metadata for individual parameters is covered separately on -[ScanArgument](scanargument.md). This page focuses on the broader scan-definition structure around -the signature, grouped positional bundles, and GUI grouping. - -## `gui_config` - -`gui_config` describes how scan inputs should be grouped in graphical interfaces. - -For example, a scan can group inputs under headings such as: - -- `Device` -- `Movement Parameters` -- `Acquisition Parameters` - -This does not change how the scan runs. It helps GUIs present scan inputs in a clear structure -instead of showing one flat list of parameters. - -## Unit Annotations - -Many scans annotate numerical inputs through `Annotated[..., ScanArgument(...)]`. - -Typical examples are: - -- `Annotated[float, ScanArgument(units=Units.s)]` for seconds -- `Annotated[float, ScanArgument(units=Units.eV)]` for electron volts -- `Annotated[float, ScanArgument(units=Units.deg)]` for degrees - -These annotations make the intended unit explicit in the scan definition. - -Scans can also reference the unit of another input directly. For example: - -- `Annotated[float, ScanArgument(reference_units="motor1")]` -- `Annotated[float, ScanArgument(reference_units="motor2")]` -- `Annotated[float, ScanArgument(reference_units="device")]` - -This means the value should use the same unit as that referenced input. So -`Annotated[float, ScanArgument(reference_units="motor1")]` means “interpret this value in the -units of `motor1`.” - -This is especially useful for position-like parameters such as starts, stops, and step sizes. - -## `scan_def` and `scan_group` - -Two pieces of scan-definition-related metadata are now handled on the client side and propagated in -the request `system_config`: - -- `scans.scan_def`, which creates a temporary scan-definition ID and wraps several scan requests into one logical definition -- `scans.scan_group`, which assigns several scan requests to one queue group - -These are context managers and decorators. They do not replace the scan class definition itself, but -they are part of how modern BEC associates related scan requests at runtime. - -## Reloading The Scan Server - -If you add a new scan or change an existing scan class, the scan server must reload that Python code -before the changes become available. - -In practice, that means you should restart or reload the scan server after editing scan -implementations. Otherwise the running server will continue using the old version of the scan. - -## Next Step - -If you want to see these ideas in a richer real scan, read the -[worked example: hexagonal scan](hexagonal-scan-example.md). - -## What To Remember - -!!! info "What to remember" - - The serialized scan signature is part of the runtime API between the scan server, clients, and GUIs. - - `arg_input` and `arg_bundle_size` define repeated positional bundles such as move targets or line-scan ranges. - - `ScanArgument` carries units, bounds, labels, and other GUI-facing metadata for individual inputs. - - `gui_config` helps GUIs present scan parameters clearly. - - Unit annotations make scan inputs more explicit and can also refer to the units of another input. - - After changing scan code, the scan server must be reloaded or restarted. diff --git a/docs/learn/scans/scan-info.md b/docs/learn/scans/scan-info.md index 7e914a0..59120b6 100644 --- a/docs/learn/scans/scan-info.md +++ b/docs/learn/scans/scan-info.md @@ -12,14 +12,7 @@ related: # Scan Info -`scan_info` is the runtime metadata object attached to every scan. - -In the current implementation, `self.scan_info` is an instance of the `ScanInfo` model created by -`ScanBase` when the scan object is initialized. The scan then keeps refining that object during -`prepare_scan` and the later lifecycle hooks. - -This is the authoritative internal description of the running scan. The queue, scan actions, -clients, bundler, and file writer all rely on it directly or on message payloads derived from it. +A scan must keep track of its runtime metadata and parameters in a structured way. In BEC, that is the role of the `ScanInfo` model. Every scan has an instance of `ScanInfo` that is created by `ScanBase` when the scan object is initialized and then updated by the concrete scan during its lifecycle. It is accessible as `self.scan_info` from any scan hook or method, although it is most commonly updated through a helper method called `update_scan_info(...)` that can update both known top-level fields and extra scan-specific parameters in one call. ## What `scan_info` Is For @@ -35,6 +28,8 @@ request arguments, device instructions, and status messages. - which request inputs the user originally sent - which extra scan-specific parameters should travel with the scan +The `ScanInfo` model is the single source of truth for the scan's runtime metadata and is the main content for published scan status messages. Any device or client that needs to know about the scan relies on it for the most accurate and up-to-date description. + ## The `ScanInfo` Model The current `ScanInfo` model contains the following fields. @@ -187,72 +182,13 @@ The current `ScanInfo` model contains the following fields. -## How Scans Fill It - -The important pattern is simple: - -1. `ScanBase` creates the initial `scan_info` object from stable request and class metadata. -2. The concrete scan updates known runtime fields through `update_scan_info(...)`. -3. Helper methods in `self.actions` also update related reporting state such as scan-report instructions and readout-priority overrides. - -`update_scan_info(...)` has one especially important behavior: if you pass a keyword that matches a -real `ScanInfo` field, that field is updated directly. If you pass a keyword that does not exist on -the model, it is stored under `additional_scan_parameters` instead. - -That means scans can attach extra scan-specific metadata without having to extend the shared model -for every new scan type. - -## `request_inputs` - -`request_inputs` stores the original request in structured form. - -BEC keeps three buckets here: - -- `arg_bundle` for repeated positional bundles -- `inputs` for fixed signature-bound inputs -- `kwargs` for extra keyword-style inputs - -This matters because the system no longer needs to reconstruct the user's request from a flattened -argument list later on. GUIs, history, file writing, and diagnostics can all refer back to the same -structured input representation. - -## `scan_info` Versus Published Scan Status Messages - -`scan_info` is the internal runtime model, but the public scan status messages are compatibility -views built from it. - -In practice: - -- the scan status message top level exposes a smaller legacy-style subset such as `scan_name`, `num_points`, `scan_parameters`, and `request_inputs` -- the `info` payload contains a full dumped view of `scan_info`, plus compatibility fields such as resolved `readout_priority` and `file_components` -- the internal `scan_info.scan_type` values are `software_triggered` and `hardware_triggered`, while the legacy top-level message field may only contain older values such as `step` or `fly` - -So when reading older code or older docs, keep the distinction clear: `scan_info` is now the -authoritative in-process model, and some message fields are compatibility projections of it rather -than the model itself. - -## Why `prepare_scan` Matters So Much - -`prepare_scan` is often where the scan's final runtime description becomes complete. - -That is usually where a scan: - -- finalizes `num_points` -- writes prepared `positions` -- computes `num_monitored_readouts` -- records timing or movement options -- publishes scan-report instructions -- selects report devices - -This is why `scan_info` shows up so often in real scan implementations: it is the shared place -where the scan turns its user inputs into a concrete runtime description. ## Next Step -After `scan_info`, continue with [scan actions](scan-actions.md). +After `scan_info`, continue with [scan actions](scan-actions.md){data-preview}. That page covers the high-level scan operations used to publish scan state and coordinate device -work. After that, move on to [scan components](scan-components.md) for the reusable scan patterns +work. After that, move on to [scan components](scan-components.md){data-preview} for the reusable scan patterns built on top of those operations. ## What To Remember @@ -261,4 +197,4 @@ built on top of those operations. - `scan_info` is the shared runtime metadata model for a scan. - `ScanBase` creates it, and concrete scans usually finish populating it during `prepare_scan`. - Known updates go to named `ScanInfo` fields; unknown updates go to `additional_scan_parameters`. - - Published scan status messages are derived from `scan_info`, but they are not a 1:1 copy of the internal model. + - Published scan status messages are derived from `scan_info`. diff --git a/docs/learn/scans/scanargument.md b/docs/learn/scans/scanargument.md index 52d4daa..63e79fc 100644 --- a/docs/learn/scans/scanargument.md +++ b/docs/learn/scans/scanargument.md @@ -2,34 +2,21 @@ related: - title: Position Generators url: learn/scans/position-generators.md - - title: Scan Definition Info - url: learn/scans/scan-definition-info.md + - title: Argument Bundles + url: learn/scans/argument-bundles.md + - title: GUI Config + url: learn/scans/gui-config.md - title: Learn by Example url: learn/scans/learn-by-example.md --- # ScanArgument -`ScanArgument(...)` is the main way a scan attaches rich metadata to one of its inputs. +`ScanArgument(...)` enables a scan to attach rich metadata to one of its inputs. -In practice, this metadata is usually carried through `Annotated[..., ScanArgument(...)]` in a -scan's `__init__` signature. That makes the input definition useful not only to Python, but also to -validation, GUIs, and client-side scan discovery. +As a result, the signature can provide more information to users and clients, such as which units an input uses, which bounds apply, and how the input should be labeled in a GUI. -## Why `ScanArgument` Matters - -Without `ScanArgument`, a scan input would mostly just have a Python type. - -With `ScanArgument`, the same input can also describe: - -- how it should be labeled in a GUI -- which units it uses -- whether it should use the units of another input -- which bounds or limits apply -- which extra explanatory text should be shown to the user - -That is what lets one scan signature serve as both an implementation interface and a user-facing -definition. +!!! Warning "It is highly recommended to use `ScanArgument` for any scan input." ## A Typical Example @@ -52,6 +39,31 @@ This says several things at once: - the value is expressed in seconds - the value must be greater than or equal to zero +## Reusing Common Annotations With `DefaultArgType` + +Writing out full `Annotated[..., ScanArgument(...)]` definitions is useful when a scan needs a +custom input definition. However, BEC already provides shared aliases for the common internal scan +parameters that are represented in [scan info](scan-info.md){data-preview} and used in many scans. + +To avoid repeating those annotations in every scan class, these common definitions are collected in +`DefaultArgType`. + +For example, the typical example above is equivalent to: + +```py +from bec_lib.scan_args import DefaultArgType + + +exp_time: DefaultArgType.ExposureTime = 0 +``` + +The same pattern is used for other internal scan parameters such as `FramesPerTrigger`, +`SettlingTime`, `SettlingTimeAfterTrigger`, `ReadoutTime`, `BurstAtEachPoint`, and also for common +boolean scan options such as `Relative`. + +This keeps scan signatures shorter while still preserving the same `ScanArgument` metadata for +validation, GUI generation, and scan discovery. + ## Common `ScanArgument` Fields Some of the most commonly used fields are listed below. @@ -89,28 +101,18 @@ Some of the most commonly used fields are listed below. units Declares the explicit unit for the input, such as seconds or degrees. - timing or physical values + user-facing display reference_units Tells BEC to interpret the value in the units of another input, such as a motor argument. - position-like inputs + user-facing display gt, ge, lt, le Applies numeric bounds to the input. validation - - reference_limits - Uses the limits of another input as a validation reference. - device-related bounds - - - alternative_group - Associates inputs that represent alternative ways to express a similar choice. - advanced GUI behavior - @@ -133,23 +135,10 @@ Reference units: Reference units are especially useful for scan inputs such as start, stop, or step size, where the value should automatically use the same unit as a related device input. -## How It Fits Into Scan Definitions - -`ScanArgument` does not replace the scan signature. It enriches it. - -The usual pattern is: - -1. define the Python parameter in `__init__` -2. wrap it in `Annotated[..., ScanArgument(...)]` -3. let BEC serialize that richer input definition for validation and GUI generation - -This is why `ScanArgument` shows up so often in real scan definitions: it is the bridge between the -scan's code-level inputs and its user-facing definition. - ## Next Step -After `ScanArgument`, continue with [scan definition info](scan-definition-info.md), which covers -the broader scan-definition model around the signature, grouped positional inputs, and `gui_config`. +After `ScanArgument`, continue with [argument bundles](argument-bundles.md){data-preview} for +repeated positional inputs and [GUI config](gui-config.md){data-preview} for graphical grouping. ## What To Remember diff --git a/docs/references/bec-core/scan-actions-methods.md b/docs/references/bec-core/scan-actions-methods.md new file mode 100644 index 0000000..0492d4c --- /dev/null +++ b/docs/references/bec-core/scan-actions-methods.md @@ -0,0 +1,160 @@ +--- +related: + - title: Scan Actions + url: learn/scans/scan-actions.md + - title: Scan Components + url: learn/scans/scan-components.md + - title: Scan Info + url: learn/scans/scan-info.md +--- + +# ScanActions Methods + +The following methods are most relevant to scan authors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRoleTypical hook
open_scan()Publishes the opening scan status message for the current scan.open_scan
stage_all_devices()Stages all enabled scan devices, with async devices handled separately for better throughput.stage
stage(...)Stages one device or a selected device list instead of the whole scan device set.stage in custom cases
pre_scan_all_devices()Runs the pre-scan device step across all enabled devices.pre_scan
pre_scan(...)Runs the pre-scan device step only for selected devices.pre_scan in custom cases
set(...)Sends coordinated set instructions to one or several devices.prepare_scan or scan_core
kickoff(...)Starts a kickoff-capable device with optional configuration parameters.scan_core or flyer setup
complete(...)Completes one device explicitly.post_scan
complete_all_devices()Completes all enabled scan devices.post_scan
read_monitored_devices()Reads the current monitored-device group and advances the monitored readout counter.scan_core
read_manually(...)Performs an explicit read and returns the result to the scan, rather than relying on the usual monitored-read path.Special cases only
publish_manual_read(...)Publishes externally collected data as the next monitored readout.Special cases only
read_baseline_devices()Reads the baseline-device group, often around scan setup or teardown.Usually prepare_scan
trigger_all_devices()Triggers all devices configured for software triggering in the scan.scan_core
unstage(...)Unstages one selected device.unstage in custom cases
unstage_all_devices()Unstages all enabled scan devices.unstage
add_scan_report_instruction_scan_progress(...)Adds a scan-progress instruction so clients can render scan progress consistently.Usually prepare_scan
add_scan_report_instruction_readback(...)Adds a live readback instruction for selected devices.Usually prepare_scan
add_scan_report_instruction_device_progress(...)Adds a device-progress instruction for devices exposing progress signals.Usually prepare_scan
set_device_readout_priority(...)Modifies which devices are treated as monitored, baseline, on-request, or async during the scan.Usually __init__ or prepare_scan
close_scan()Finalizes monitored-readout counts, checks cleanup state, and publishes the closing scan status.close_scan
check_for_unchecked_statuses()Warns about unfinished or unchecked status objects and waits on remaining work when needed.close_scan or cleanup
add_device_with_required_response(...)Marks devices whose instructions must emit explicit response messages.Special cases only
rpc_call(...)Makes a low-level RPC call to a device-server method and returns the result.Advanced cases only
send_client_info(...)Sends an informational message to clients, for example for GUI status updates.Any hook when useful
+ +!!! tip + This table is meant as a reference, not as something to memorize in one pass. For most readers, + it is more useful to return here while reading or writing concrete scan implementations. diff --git a/zensical.toml b/zensical.toml index f281432..fd82a14 100644 --- a/zensical.toml +++ b/zensical.toml @@ -126,16 +126,26 @@ nav = [ ] }, ] }, { "Scans" = [ - { "Introduction to Scans" = "learn/scans/introduction.md" }, - { "Scan Lifecycle" = "learn/scans/lifecycle.md" }, - { "Learn by Example" = "learn/scans/learn-by-example.md" }, - { "Scan Info" = "learn/scans/scan-info.md" }, - { "Scan Actions" = "learn/scans/scan-actions.md" }, - { "Scan Components" = "learn/scans/scan-components.md" }, - { "Position Generators" = "learn/scans/position-generators.md" }, - { "Fast Axis and Slow Axis" = "learn/scans/fast-axis-slow-axis.md" }, - { "ScanArgument" = "learn/scans/scanargument.md" }, - { "Scan Definition Info" = "learn/scans/scan-definition-info.md" }, + { "Overview" = [ + { "Introduction to Scans" = "learn/scans/introduction.md" }, + { "Scan Lifecycle" = "learn/scans/lifecycle.md" }, + { "Learn by Example" = "learn/scans/learn-by-example.md" }, + { "Motions" = "learn/scans/motions.md" }, + ] }, + { "Runtime Model" = [ + { "Scan Info" = "learn/scans/scan-info.md" }, + { "Scan Actions" = "learn/scans/scan-actions.md" }, + { "Scan Components" = "learn/scans/scan-components.md" }, + ] }, + { "Defining Scan Inputs" = [ + { "ScanArgument" = "learn/scans/scanargument.md" }, + { "Argument Bundles" = "learn/scans/argument-bundles.md" }, + { "GUI Config" = "learn/scans/gui-config.md" }, + ] }, + { "Motion Patterns" = [ + { "Position Generators" = "learn/scans/position-generators.md" }, + { "Fast Axis and Slow Axis" = "learn/scans/fast-axis-slow-axis.md" }, + ] }, ] }, { "File Writing" = [ { "Introduction" = "learn/file-writer/introduction.md" }, @@ -162,10 +172,11 @@ nav = [ ] }, { References = [ "references/index.md", - # { "BEC Core" = [ TODO add references again once available - # { "Scan Base and stubs" = "references/bec-core/scan-base-and-stubs.md" }, - # { "Scans" = "references/bec-core/scans.md" }, - # ] }, + { "BEC Core" = [ + { "Scan Base and stubs" = "references/bec-core/scan-base-and-stubs.md" }, + { "Scans" = "references/bec-core/scans.md" }, + { "ScanActions Methods" = "references/bec-core/scan-actions-methods.md" }, + ] }, { "BEC Widgets" = [ { "GUI RPC Interface" = "references/bec-widgets/gui-rpc-interface.md" }, ] }, From 02d9f846df22fdb0282f5837f530585251a17bbc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 9 May 2026 12:32:56 +0200 Subject: [PATCH 5/6] f - add tip to not always write scans --- docs/learn/scans/introduction.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/learn/scans/introduction.md b/docs/learn/scans/introduction.md index 5a3b348..62e5611 100644 --- a/docs/learn/scans/introduction.md +++ b/docs/learn/scans/introduction.md @@ -33,6 +33,11 @@ That is true even when the middle of the scan is very different. For example, a line scan, a grid scan, and a continuous scan may move differently, but they still fit into one common scan framework. +!!! tip "Most custom workflows do not need a new scan" + If your goal is to combine several existing scan calls, react to results, or add beamline-specific decision logic, it is usually simpler to write a normal Python script around the built-in scans first. + + Create a new scan class only when you need new server-side scan behavior, lifecycle integration, or a reusable scan that should appear as its own `scans.(...)` entry. + ## What Happens During A Scan !!! Note "Dataflow during a scan" From 8d3ca027caebfe2dc3e1e5c5eec901d95f882d9f Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 11 May 2026 19:50:31 +0200 Subject: [PATCH 6/6] f --- docs/learn/scans/introduction.md | 34 +++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/learn/scans/introduction.md b/docs/learn/scans/introduction.md index 62e5611..8aae883 100644 --- a/docs/learn/scans/introduction.md +++ b/docs/learn/scans/introduction.md @@ -33,6 +33,34 @@ That is true even when the middle of the scan is very different. For example, a line scan, a grid scan, and a continuous scan may move differently, but they still fit into one common scan framework. +## Client and server responsibilities + +One of the most important design choices in BEC is that the client and the scan server do different +jobs. + +The client is the user-facing side. It learns which scans are currently available from the scan +server and exposes them dynamically as `scans.(...)`. That means the client does not hardcode +the live scan definitions. Instead, it uses the signatures, documentation, and GUI metadata +published by the server at runtime. + +When you call a scan from the client, the client does not run the scan logic locally. It validates +and packages your inputs, adds request metadata, and sends a scan request to the scan server. + +The scan server is the execution side. It owns the registered scan classes, validates incoming scan +requests against those server-side definitions, puts accepted requests into the scan queue, and +hands them to a scan worker. The worker then instantiates the scan class and runs its lifecycle. + +This separation brings several practical benefits: + +- scans can be improved or extended on the server while clients pick up the updated scan definitions and signatures dynamically +- queueing, execution, progress reporting, and scan data production can be coordinated in one place instead of being split across independently executing clients +- users can run scans from lightweight client scripts without moving hardware logic into each script +- different clients, languages, and machines can submit scan requests without needing to execute the full scan logic locally +- different clients and interfaces can share the same server-side scan implementation and runtime model + +In other words, `scans.line_scan(...)` in the client is a request interface, while the scan class +implementation lives and runs on the scan server. + !!! tip "Most custom workflows do not need a new scan" If your goal is to combine several existing scan calls, react to results, or add beamline-specific decision logic, it is usually simpler to write a normal Python script around the built-in scans first. @@ -47,10 +75,10 @@ At a high level, a scan in BEC follows this path: 1. The scan server publishes the available scan classes together with their serialized signatures, grouped inputs, and GUI metadata. 1. The client exposes those scans dynamically, so commands such as `scans.line_scan(...)` use the current server-side definition. -1. When a scan is called, the client sends a request to the server with the scan class's name and the provided arguments. -1. On the server, the scan is assembled from the scan class and the provided arguments, and then it is put in the scan server queue. +1. When a scan is called, the client validates and bundles the arguments, adds request metadata, and sends a request to the server with the scan class's name. +1. On the server, the request is checked against the server-side scan definitions and, if accepted, put into the scan server queue. 1. Once it is the scan's turn to run, the scan server queue hands over the request to a scan worker. -1. The scan worker runs the scan class's lifecycle hooks. +1. The scan worker instantiates the scan class on the server and runs its lifecycle hooks. 1. During the scan, the scan class may use scan actions or components to trigger readouts, move devices, or run custom logic at each scan point. 1. Devices publish readouts and status updates. 1. The scan bundler groups those readouts into logical scan points.