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/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/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/introduction.md b/docs/learn/scans/introduction.md new file mode 100644 index 0000000..8aae883 --- /dev/null +++ b/docs/learn/scans/introduction.md @@ -0,0 +1,118 @@ +--- +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 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. + + +## 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. + +## 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. + + 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" + 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. +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 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 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. +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 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 + +!!! 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 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 new file mode 100644 index 0000000..1dda7b8 --- /dev/null +++ b/docs/learn/scans/learn-by-example.md @@ -0,0 +1,273 @@ +--- +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: GUI Config + url: learn/scans/gui-config.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 shared default argument types +- `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 + +import numpy as np + +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)! + + gui_config = { # (3)! + "Scan Parameters": [ + "exp_time", + "frames_per_trigger", + "settling_time", + "settling_time_after_trigger", + "readout_time", + "burst_at_each_point", + ] + } + + def __init__( + 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 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. 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` + +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( + positions=np.array([]), + num_points=1, + num_monitored_readouts=self.burst_at_each_point, + ) + + 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) +``` + +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. + +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. + +## `open_scan` + +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() +``` + +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.burst_at_each_point): + self.at_each_point() +``` + +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. + +## `at_each_point` + +The actual trigger-and-read work happens in a separate per-point hook. + +```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() +``` + +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: + self._baseline_readout_status.wait() + self.actions.close_scan() + 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 +``` + +In this case, there is no special cleanup needed. + + +## What This Example Shows + +The `acquire` scan is a compact example of the shared scan shape: + +- 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 +- 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){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){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 + +!!! info "What to remember" + - 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/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/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..b8c5c22 --- /dev/null +++ b/docs/learn/scans/scan-actions.md @@ -0,0 +1,43 @@ +--- +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 + +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. + +!!! 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 Scan Actions Are For + +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 + +## 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 }. + +!!! 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 + +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-info.md b/docs/learn/scans/scan-info.md new file mode 100644 index 0000000..59120b6 --- /dev/null +++ b/docs/learn/scans/scan-info.md @@ -0,0 +1,200 @@ +--- +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 + +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 + +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 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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
+ + +## Next Step + +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){data-preview} 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`. diff --git a/docs/learn/scans/scanargument.md b/docs/learn/scans/scanargument.md new file mode 100644 index 0000000..63e79fc --- /dev/null +++ b/docs/learn/scans/scanargument.md @@ -0,0 +1,148 @@ +--- +related: + - title: Position Generators + url: learn/scans/position-generators.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(...)` enables a scan to attach rich metadata to one of its inputs. + +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. + +!!! Warning "It is highly recommended to use `ScanArgument` for any scan input." + +## 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 + +## 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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.user-facing display
reference_unitsTells BEC to interpret the value in the units of another input, such as a motor argument.user-facing display
gt, ge, lt, leApplies numeric bounds to the input.validation
+ +## 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. + +## Next Step + +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 + +!!! 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/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 93e940f..fd82a14 100644 --- a/zensical.toml +++ b/zensical.toml @@ -125,8 +125,28 @@ nav = [ { "Simulated Devices" = "learn/devices/simulated-devices.md" }, ] }, ] }, - #{ "Scans and execution" = [ TODO fill with content - #] }, + { "Scans" = [ + { "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" }, { "Where Files Are Written" = "learn/file-writer/where-files-are-written.md" }, @@ -152,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" }, ] },