feat(bec-signals): Add acquisition group to BECMessageSignal and SignalInfo

This commit is contained in:
2025-10-15 12:42:54 +02:00
committed by Christian Appel
parent 297c5eee38
commit 9b51b22671

View File

@@ -3,6 +3,7 @@ Module for custom BEC signals, that wrap around ophyd.Signal.
These signals emit BECMessage objects, which comply with the BEC message system. These signals emit BECMessage objects, which comply with the BEC message system.
""" """
from time import time
from typing import Any, Callable, Literal, Type from typing import Any, Callable, Literal, Type
import numpy as np import numpy as np
@@ -10,6 +11,7 @@ from bec_lib import messages
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from ophyd import DeviceStatus, Kind, Signal from ophyd import DeviceStatus, Kind, Signal
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field, ValidationError
from typeguard import typechecked
logger = bec_logger.logger logger = bec_logger.logger
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
@@ -60,6 +62,14 @@ class SignalInfo(BaseModel):
default=None, default=None,
description="Metadata for the signal, which can include additional information about the signal's properties.", description="Metadata for the signal, which can include additional information about the signal's properties.",
) )
acquisition_group: Literal["baseline", "monitored"] | str | None = Field(
default=None,
description="""Specifies the acquisition group of the signal.
It can be in sync with 'baseline' or 'monitored' groups mapping readoutPriority.
Or mapped to a custom tag that allows grouping signals for acquisition and plotting.
If None, the signal does not belong to any specific acquisition group.
""",
)
class BECMessageSignal(Signal): class BECMessageSignal(Signal):
@@ -79,6 +89,7 @@ class BECMessageSignal(Signal):
ndim: Literal[0, 1, 2] | None = None, ndim: Literal[0, 1, 2] | None = None,
scope: Literal["scan", "continuous"] = "scan", scope: Literal["scan", "continuous"] = "scan",
role: Literal["main", "preview", "diagnostic", "file event", "progress"] = "main", role: Literal["main", "preview", "diagnostic", "file event", "progress"] = "main",
acquisition_group: Literal["baseline", "monitored"] | str | None = None,
enabled: bool = True, enabled: bool = True,
signals: ( signals: (
Callable[[], list[str]] Callable[[], list[str]]
@@ -116,6 +127,7 @@ class BECMessageSignal(Signal):
self.scope = scope self.scope = scope
self.role = role self.role = role
self.enabled = enabled self.enabled = enabled
self.acquisition_group = acquisition_group
self.signals = self._unify_signals(signals) self.signals = self._unify_signals(signals)
self.signal_metadata = signal_metadata self.signal_metadata = signal_metadata
self._bec_message_type = bec_message_type self._bec_message_type = bec_message_type
@@ -169,6 +181,7 @@ class BECMessageSignal(Signal):
enabled=self.enabled, enabled=self.enabled,
signals=self.signals, signals=self.signals,
signal_metadata=self.signal_metadata, signal_metadata=self.signal_metadata,
acquisition_group=self.acquisition_group,
).model_dump() ).model_dump()
return out return out
@@ -618,7 +631,7 @@ class PreviewSignal(BECMessageSignal):
class DynamicSignal(BECMessageSignal): class DynamicSignal(BECMessageSignal):
"""Signal to emit dynamic device data.""" """Signal group to emit dynamic device signal data."""
strict_signal_validation = False # Disable strict signal validation strict_signal_validation = False # Disable strict signal validation
@@ -628,17 +641,29 @@ class DynamicSignal(BECMessageSignal):
name: str, name: str,
signals: list[str] | Callable[[], list[str]] | None = None, signals: list[str] | Callable[[], list[str]] | None = None,
value: messages.DeviceMessage | dict | None = None, value: messages.DeviceMessage | dict | None = None,
async_update: dict | None = None, async_update: dict[Literal["type", "max_size", "index"], Any] | None = None,
acquisition_group: Literal["baseline", "monitored"] | str | None = None,
**kwargs, **kwargs,
): ):
""" """
Create a new DynamicSignal object. Create a new DynamicSignal object.
Args: Args:
name (str): The name of the signal. name (str): The name of the signal group.
signal_names (list[str] | Callable): The names of all signals. Can be a list or a callable. signal_names (list[str] | Callable): Names of all signals. Can be a list or a callable.
value (DeviceMessage | dict | None): The initial value of the signal. Defaults to None. value (DeviceMessage | dict | None): The initial value of the signal. Defaults to None.
async_update (dict | None): Additional metadata for asynchronous updates. Defaults to None. acquisition_group (Literal["baseline", "monitored"] | str | None): The acquisition group of the signal group.
async_update (dict | None): Additional metadata for asynchronous updates.
There are three relevant keys "type", "max_size" and "index".
"type" (str) : Can be one of "add", "add_slice" or "replace". This defines how the new data is added to the existing dataset.
"add" : Appends data to the existing dataset. The data is always appended to the first axis.
"add_slice" : Appends data to the existing dataset, but allows specifying a slice.
The slice is defined by the "index" key.
"replace" : Replaces the existing dataset with the new data.
"max_size" (list[int | None]): Required for type 'add' and 'add_slice'. It defines where the data is added. For a 1D dataset,
it should be [None]. For a 1D dataset with 3000 elements, it should be [None, 3000].
For a 2D dataset with 3000x3000 elements, it should be [None, 3000, 3000].
"index" (int): Only required for type 'add_slice'. It defines the index where the data is added.
""" """
self.async_update = async_update self.async_update = async_update
@@ -653,15 +678,18 @@ class DynamicSignal(BECMessageSignal):
signals=signals, signals=signals,
value=value, value=value,
bec_message_type=kwargs.pop("bec_message_type", messages.DeviceMessage), bec_message_type=kwargs.pop("bec_message_type", messages.DeviceMessage),
acquisition_group=acquisition_group,
**kwargs, **kwargs,
) )
@typechecked
def put( def put(
self, self,
value: messages.DeviceMessage | dict[str, dict[Literal["value", "timestamp"], Any]], value: messages.DeviceMessage | dict[str, dict[Literal["value", "timestamp"], Any]],
*, *,
metadata: dict | None = None, metadata: dict | None = None,
async_update: dict | None = None, async_update: dict[Literal["type", "max_size", "index"], Any] | None = None,
acquisition_group: Literal["baseline", "monitored"] | str | None = None,
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@@ -674,21 +702,33 @@ class DynamicSignal(BECMessageSignal):
Args: Args:
value (dict | DeviceMessage): The dynamic device data. value (dict | DeviceMessage): The dynamic device data.
metadata (dict | None): Additional metadata. metadata (dict | None): Additional metadata.
async_update (dict[Literal["type", "max_size", "index"], Any] | None): Additional metadata for asynchronous updates.
acquisition_group (Literal["baseline", "monitored"] | str | None): The acquisition group of the signal group.
""" """
if isinstance(value, messages.DeviceMessage): if isinstance(value, messages.DeviceMessage):
if metadata is not None or async_update is not None: if metadata is not None or async_update is not None or acquisition_group is not None:
logger.warning( logger.warning(
"Ignoring metadata and async_update arguments when value is a DeviceMessage." "Ignoring metadata, async_update and acquisition_group arguments when value is a DeviceMessage."
) )
self._check_signals(value) self._check_signals(value)
return super().put(value, **kwargs) return super().put(value, **kwargs)
try: try:
metadata = metadata or {} metadata = metadata or {}
if "async_update" not in metadata: if async_update is not None:
if async_update is not None: metadata["async_update"] = async_update
metadata["async_update"] = async_update else:
elif self.async_update is not None: metadata["async_update"] = self.async_update
metadata["async_update"] = self.async_update if not metadata.get("async_update"):
raise ValueError(
f"Async update must be provided for signal {self.name} of class {self.__class__.__name__}."
)
else:
pass
# TODO #627 Issue in BEC: Validate async_update --> bec_lib
if acquisition_group is not None:
metadata["acquisition_group"] = acquisition_group
elif self.acquisition_group is not None:
metadata["acquisition_group"] = self.acquisition_group
msg = messages.DeviceMessage(signals=value, metadata=metadata) msg = messages.DeviceMessage(signals=value, metadata=metadata)
except ValidationError as exc: except ValidationError as exc:
@@ -723,7 +763,8 @@ class DynamicSignal(BECMessageSignal):
value: messages.DeviceMessage | dict[str, dict[Literal["value"], Any]], value: messages.DeviceMessage | dict[str, dict[Literal["value"], Any]],
*, *,
metadata: dict | None = None, metadata: dict | None = None,
async_update: dict | None = None, async_update: dict[Literal["type", "max_size", "index"], Any] | None = None,
acquisition_group: Literal["baseline", "monitored"] | str | None = None,
**kwargs, **kwargs,
) -> DeviceStatus: ) -> DeviceStatus:
""" """
@@ -737,7 +778,13 @@ class DynamicSignal(BECMessageSignal):
value (dict | DeviceMessage) : The dynamic device data. value (dict | DeviceMessage) : The dynamic device data.
metadata (dict | None) : Additional metadata. metadata (dict | None) : Additional metadata.
""" """
self.put(value, metadata=metadata, async_update=async_update, **kwargs) self.put(
value,
metadata=metadata,
async_update=async_update,
acquisition_group=acquisition_group,
**kwargs,
)
status = DeviceStatus(device=self) status = DeviceStatus(device=self)
status.set_finished() status.set_finished()
return status return status