1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

refactor: global refactoring to use device-signal pair names

This commit is contained in:
2026-02-02 15:29:32 +01:00
committed by Jan Wyzula
parent 0315a10602
commit 017faf33e2
38 changed files with 1367 additions and 1388 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from enum import IntEnum
from functools import partial
from typing import TYPE_CHECKING, List, Tuple
from typing import TYPE_CHECKING, Any, List, Tuple
from bec_lib.logger import bec_logger
from bec_qthemes import apply_theme, material_icon

View File

@@ -102,17 +102,17 @@ class WaveformViewPopup(ViewBase): # pragma: no cover
self.device_edit.insertItem(0, "")
self.device_edit.setEditable(True)
self.device_edit.setCurrentIndex(0)
self.entry_edit = SignalComboBox(parent=self)
self.entry_edit.include_config_signals = False
self.entry_edit.insertItem(0, "")
self.entry_edit.setEditable(True)
self.device_edit.currentTextChanged.connect(self.entry_edit.set_device)
self.device_edit.device_reset.connect(self.entry_edit.reset_selection)
self.signal_edit = SignalComboBox(parent=self)
self.signal_edit.include_config_signals = False
self.signal_edit.insertItem(0, "")
self.signal_edit.setEditable(True)
self.device_edit.currentTextChanged.connect(self.signal_edit.set_device)
self.device_edit.device_reset.connect(self.signal_edit.reset_selection)
form = QFormLayout()
form.addRow(label)
form.addRow("Device", self.device_edit)
form.addRow("Signal", self.entry_edit)
form.addRow("Signal", self.signal_edit)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog)
buttons.accepted.connect(dialog.accept)
@@ -124,7 +124,7 @@ class WaveformViewPopup(ViewBase): # pragma: no cover
if dialog.exec_() == QDialog.Accepted:
self.waveform.plot(
y_name=self.device_edit.currentText(), y_entry=self.entry_edit.currentText()
device_y=self.device_edit.currentText(), signal_y=self.signal_edit.currentText()
)
@SafeSlot()
@@ -249,7 +249,7 @@ class WaveformViewInline(ViewBase): # pragma: no cover
dev = self.device_edit.currentText()
sig = self.entry_edit.currentText()
if dev and sig:
self.waveform.plot(y_name=dev, y_entry=sig)
self.waveform.plot(device_y=dev, signal_y=sig)
self.stack.setCurrentIndex(1)
def _show_waveform_without_changes(self):

View File

@@ -1944,12 +1944,12 @@ class Heatmap(RPCBase):
@rpc_call
def plot(
self,
x_name: "str",
y_name: "str",
z_name: "str",
x_entry: "None | str" = None,
y_entry: "None | str" = None,
z_entry: "None | str" = None,
device_x: "str",
device_y: "str",
device_z: "str",
signal_x: "None | str" = None,
signal_y: "None | str" = None,
signal_z: "None | str" = None,
color_map: "str | None" = "plasma",
validate_bec: "bool" = True,
interpolation: "Literal['linear', 'nearest'] | None" = None,
@@ -1963,12 +1963,12 @@ class Heatmap(RPCBase):
Plot the heatmap with the given x, y, and z data.
Args:
x_name (str): The name of the x-axis signal.
y_name (str): The name of the y-axis signal.
z_name (str): The name of the z-axis signal.
x_entry (str | None): The entry for the x-axis signal.
y_entry (str | None): The entry for the y-axis signal.
z_entry (str | None): The entry for the z-axis signal.
device_x (str): The name of the x-axis device signal.
device_y (str): The name of the y-axis device signal.
device_z (str): The name of the z-axis device signal.
signal_x (str | None): The entry for the x-axis device signal.
signal_y (str | None): The entry for the y-axis device signal.
signal_z (str | None): The entry for the z-axis device signal.
color_map (str | None): The color map to use for the heatmap.
validate_bec (bool): Whether to validate the entries against BEC signals.
interpolation (Literal["linear", "nearest"] | None): The interpolation method to use.
@@ -1981,84 +1981,84 @@ class Heatmap(RPCBase):
@property
@rpc_call
def x_device_name(self) -> "str":
def device_x(self) -> "str":
"""
Device name for the X axis.
"""
@x_device_name.setter
@device_x.setter
@rpc_call
def x_device_name(self) -> "str":
def device_x(self) -> "str":
"""
Device name for the X axis.
"""
@property
@rpc_call
def x_device_entry(self) -> "str":
def signal_x(self) -> "str":
"""
Signal entry for the X axis device.
"""
@x_device_entry.setter
@signal_x.setter
@rpc_call
def x_device_entry(self) -> "str":
def signal_x(self) -> "str":
"""
Signal entry for the X axis device.
"""
@property
@rpc_call
def y_device_name(self) -> "str":
def device_y(self) -> "str":
"""
Device name for the Y axis.
"""
@y_device_name.setter
@device_y.setter
@rpc_call
def y_device_name(self) -> "str":
def device_y(self) -> "str":
"""
Device name for the Y axis.
"""
@property
@rpc_call
def y_device_entry(self) -> "str":
def signal_y(self) -> "str":
"""
Signal entry for the Y axis device.
"""
@y_device_entry.setter
@signal_y.setter
@rpc_call
def y_device_entry(self) -> "str":
def signal_y(self) -> "str":
"""
Signal entry for the Y axis device.
"""
@property
@rpc_call
def z_device_name(self) -> "str":
def device_z(self) -> "str":
"""
Device name for the Z (color) axis.
"""
@z_device_name.setter
@device_z.setter
@rpc_call
def z_device_name(self) -> "str":
def device_z(self) -> "str":
"""
Device name for the Z (color) axis.
"""
@property
@rpc_call
def z_device_entry(self) -> "str":
def signal_z(self) -> "str":
"""
Signal entry for the Z (color) axis device.
"""
@z_device_entry.setter
@signal_z.setter
@rpc_call
def z_device_entry(self) -> "str":
def signal_z(self) -> "str":
"""
Signal entry for the Z (color) axis device.
"""
@@ -2480,28 +2480,28 @@ class Image(RPCBase):
@property
@rpc_call
def device_name(self) -> "str":
def device(self) -> "str":
"""
The name of the device to monitor for image data.
"""
@device_name.setter
@device.setter
@rpc_call
def device_name(self) -> "str":
def device(self) -> "str":
"""
The name of the device to monitor for image data.
"""
@property
@rpc_call
def device_entry(self) -> "str":
def signal(self) -> "str":
"""
The signal/entry name to monitor on the device.
"""
@device_entry.setter
@signal.setter
@rpc_call
def device_entry(self) -> "str":
def signal(self) -> "str":
"""
The signal/entry name to monitor on the device.
"""
@@ -2609,8 +2609,8 @@ class Image(RPCBase):
@rpc_call
def image(
self,
device_name: "str | None" = None,
device_entry: "str | None" = None,
device: "str | None" = None,
signal: "str | None" = None,
color_map: "str | None" = None,
color_bar: "Literal['simple', 'full'] | None" = None,
vrange: "tuple[int, int] | None" = None,
@@ -2619,8 +2619,8 @@ class Image(RPCBase):
Set the image source and update the image.
Args:
device_name(str|None): The name of the device to monitor. If None or empty string, the current monitor will be disconnected.
device_entry(str|None): The signal/entry name to monitor on the device.
device(str|None): The name of the device to monitor. If None or empty string, the current monitor will be disconnected.
signal(str|None): The signal/entry name to monitor on the device.
color_map(str): The color map to use for the image.
color_bar(str): The type of color bar to use. Options are "simple" or "full".
vrange(tuple): The range of values to use for the color map.
@@ -3632,14 +3632,14 @@ class MotorMap(RPCBase):
@rpc_call
def map(
self, x_name: "str", y_name: "str", validate_bec: "bool" = True, suppress_errors=False
self, device_x: "str", device_y: "str", validate_bec: "bool" = True, suppress_errors=False
) -> "None":
"""
Set the x and y motor names.
Args:
x_name(str): The name of the x motor.
y_name(str): The name of the y motor.
device_x(str): The name of the x motor.
device_y(str): The name of the y motor.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
suppress_errors(bool, optional): If True, suppress errors during validation. Defaults to False. Used for properties setting. If the validation fails, the changes are not applied.
"""
@@ -3661,28 +3661,28 @@ class MotorMap(RPCBase):
@property
@rpc_call
def x_motor(self) -> "str":
def device_x(self) -> "str":
"""
Name of the motor shown on the X axis.
"""
@x_motor.setter
@device_x.setter
@rpc_call
def x_motor(self) -> "str":
def device_x(self) -> "str":
"""
Name of the motor shown on the X axis.
"""
@property
@rpc_call
def y_motor(self) -> "str":
def device_y(self) -> "str":
"""
Name of the motor shown on the Y axis.
"""
@y_motor.setter
@device_y.setter
@rpc_call
def y_motor(self) -> "str":
def device_y(self) -> "str":
"""
Name of the motor shown on the Y axis.
"""
@@ -5227,12 +5227,12 @@ class ScatterWaveform(RPCBase):
@rpc_call
def plot(
self,
x_name: "str",
y_name: "str",
z_name: "str",
x_entry: "None | str" = None,
y_entry: "None | str" = None,
z_entry: "None | str" = None,
device_x: "str",
device_y: "str",
device_z: "str",
signal_x: "None | str" = None,
signal_y: "None | str" = None,
signal_z: "None | str" = None,
color_map: "str | None" = "plasma",
label: "str | None" = None,
validate_bec: "bool" = True,
@@ -5241,12 +5241,12 @@ class ScatterWaveform(RPCBase):
Plot the data from the device signals.
Args:
x_name (str): The name of the x device signal.
y_name (str): The name of the y device signal.
z_name (str): The name of the z device signal.
x_entry (None | str): The x entry of the device signal.
y_entry (None | str): The y entry of the device signal.
z_entry (None | str): The z entry of the device signal.
device_x (str): The name of the x device signal.
device_y (str): The name of the y device signal.
device_z (str): The name of the z device signal.
signal_x (None | str): The x entry of the device signal.
signal_y (None | str): The y entry of the device signal.
signal_z (None | str): The z entry of the device signal.
color_map (str | None): The color map of the scatter waveform.
label (str | None): The label of the curve.
validate_bec (bool): Whether to validate the device signals with current BEC instance.
@@ -5274,84 +5274,84 @@ class ScatterWaveform(RPCBase):
@property
@rpc_call
def x_device_name(self) -> "str":
def device_x(self) -> "str":
"""
Device name for the X axis.
"""
@x_device_name.setter
@device_x.setter
@rpc_call
def x_device_name(self) -> "str":
def device_x(self) -> "str":
"""
Device name for the X axis.
"""
@property
@rpc_call
def x_device_entry(self) -> "str":
def signal_x(self) -> "str":
"""
Signal entry for the X axis device.
"""
@x_device_entry.setter
@signal_x.setter
@rpc_call
def x_device_entry(self) -> "str":
def signal_x(self) -> "str":
"""
Signal entry for the X axis device.
"""
@property
@rpc_call
def y_device_name(self) -> "str":
def device_y(self) -> "str":
"""
Device name for the Y axis.
"""
@y_device_name.setter
@device_y.setter
@rpc_call
def y_device_name(self) -> "str":
def device_y(self) -> "str":
"""
Device name for the Y axis.
"""
@property
@rpc_call
def y_device_entry(self) -> "str":
def signal_y(self) -> "str":
"""
Signal entry for the Y axis device.
"""
@y_device_entry.setter
@signal_y.setter
@rpc_call
def y_device_entry(self) -> "str":
def signal_y(self) -> "str":
"""
Signal entry for the Y axis device.
"""
@property
@rpc_call
def z_device_name(self) -> "str":
def device_z(self) -> "str":
"""
Device name for the Z (color) axis.
"""
@z_device_name.setter
@device_z.setter
@rpc_call
def z_device_name(self) -> "str":
def device_z(self) -> "str":
"""
Device name for the Z (color) axis.
"""
@property
@rpc_call
def z_device_entry(self) -> "str":
def signal_z(self) -> "str":
"""
Signal entry for the Z (color) axis device.
"""
@z_device_entry.setter
@signal_z.setter
@rpc_call
def z_device_entry(self) -> "str":
def signal_z(self) -> "str":
"""
Signal entry for the Z (color) axis device.
"""
@@ -5879,14 +5879,14 @@ class Waveform(RPCBase):
@property
@rpc_call
def x_entry(self) -> "str | None":
def signal_x(self) -> "str | None":
"""
The x signal name.
"""
@x_entry.setter
@signal_x.setter
@rpc_call
def x_entry(self) -> "str | None":
def signal_x(self) -> "str | None":
"""
The x signal name.
"""
@@ -5953,10 +5953,10 @@ class Waveform(RPCBase):
arg1: "list | np.ndarray | str | None" = None,
y: "list | np.ndarray | None" = None,
x: "list | np.ndarray | None" = None,
x_name: "str | None" = None,
y_name: "str | None" = None,
x_entry: "str | None" = None,
y_entry: "str | None" = None,
device_x: "str | None" = None,
device_y: "str | None" = None,
signal_x: "str | None" = None,
signal_y: "str | None" = None,
color: "str | None" = None,
label: "str | None" = None,
dap: "str | None" = None,
@@ -5968,17 +5968,17 @@ class Waveform(RPCBase):
Plot a curve to the plot widget.
Args:
arg1(list | np.ndarray | str | None): First argument, which can be x data, y data, or y_name.
arg1(list | np.ndarray | str | None): First argument, which can be x data, y data, or device_y.
y(list | np.ndarray): Custom y data to plot.
x(list | np.ndarray): Custom y data to plot.
x_name(str): Name of the x signal.
device_x(str): Name of the x signal.
- "auto": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of a device from BEC.
y_name(str): The name of the device for the y-axis.
x_entry(str): The name of the entry for the x-axis.
y_entry(str): The name of the entry for the y-axis.
device_y(str): The name of the device for the y-axis.
signal_x(str): The name of the entry for the x-axis.
signal_y(str): The name of the entry for the y-axis.
color(str): The color of the curve.
label(str): The label of the curve.
dap(str): The dap model to use for the curve. When provided, a DAP curve is

View File

@@ -256,8 +256,8 @@ class AutoUpdates(BECMainWindow):
# as the label and title
wf.clear_all()
wf.plot(
x_name=dev_x,
y_name=dev_y,
device_x=dev_x,
device_y=dev_y,
label=f"Scan {info.scan_number} - {dev_y}",
title=f"Scan {info.scan_number}",
x_label=dev_x,
@@ -265,7 +265,7 @@ class AutoUpdates(BECMainWindow):
)
logger.info(
f"Auto Update [simple_line_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}"
f"Auto Update [simple_line_scan]: Started plot with: device_x={dev_x}, device_y={dev_y}"
)
def simple_grid_scan(self, info: ScanStatusMessage) -> None:
@@ -288,11 +288,14 @@ class AutoUpdates(BECMainWindow):
# Clear the scatter waveform widget and plot the data
scatter.clear_all()
scatter.plot(
x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
device_x=dev_x,
device_y=dev_y,
device_z=dev_z,
label=f"Scan {info.scan_number} - {dev_z}",
)
logger.info(
f"Auto Update [simple_grid_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}, z_name={dev_z}"
f"Auto Update [simple_grid_scan]: Started plot with: device_x={dev_x}, device_y={dev_y}, device_z={dev_z}"
)
def best_effort(self, info: ScanStatusMessage) -> None:
@@ -317,15 +320,17 @@ class AutoUpdates(BECMainWindow):
# Clear the waveform widget and plot the data
wf.clear_all()
wf.plot(
x_name=dev_x,
y_name=dev_y,
device_x=dev_x,
device_y=dev_y,
label=f"Scan {info.scan_number} - {dev_y}",
title=f"Scan {info.scan_number}",
x_label=dev_x,
y_label=dev_y,
)
logger.info(f"Auto Update [best_effort]: Started plot with: x_name={dev_x}, y_name={dev_y}")
logger.info(
f"Auto Update [best_effort]: Started plot with: device_x={dev_x}, device_y={dev_y}"
)
#######################################################################
################# GUI Callbacks #######################################

View File

@@ -266,6 +266,7 @@ class DeviceSignalInputBase(BECWidget):
Args:
device(str): Device to validate.
raise_on_false(bool): Raise ValueError if device is not found.
"""
if device in self.dev:
return True

View File

@@ -35,8 +35,8 @@ logger = bec_logger.logger
class HeatmapDeviceSignal(BaseModel):
"""The configuration of a signal in the scatter waveform widget."""
name: str
entry: str
device: str
signal: str
model_config: dict = {"validate_assignment": True}
@@ -65,13 +65,13 @@ class HeatmapConfig(ConnectionConfig):
lock_aspect_ratio: bool = Field(
False, description="Whether to lock the aspect ratio of the image."
)
x_device: HeatmapDeviceSignal | None = Field(
device_x: HeatmapDeviceSignal | None = Field(
None, description="The x device signal of the heatmap."
)
y_device: HeatmapDeviceSignal | None = Field(
device_y: HeatmapDeviceSignal | None = Field(
None, description="The y device signal of the heatmap."
)
z_device: HeatmapDeviceSignal | None = Field(
device_z: HeatmapDeviceSignal | None = Field(
None, description="The z device signal of the heatmap."
)
@@ -204,18 +204,18 @@ class Heatmap(ImageBase):
"rois",
"plot",
# Device properties
"x_device_name",
"x_device_name.setter",
"x_device_entry",
"x_device_entry.setter",
"y_device_name",
"y_device_name.setter",
"y_device_entry",
"y_device_entry.setter",
"z_device_name",
"z_device_name.setter",
"z_device_entry",
"z_device_entry.setter",
"device_x",
"device_x.setter",
"signal_x",
"signal_x.setter",
"device_y",
"device_y.setter",
"signal_y",
"signal_y.setter",
"device_z",
"device_z.setter",
"signal_z",
"signal_z.setter",
]
PLUGIN = True
@@ -238,9 +238,9 @@ class Heatmap(ImageBase):
interpolation="linear",
oversampling_factor=1.0,
lock_aspect_ratio=False,
x_device=None,
y_device=None,
z_device=None,
device_x=None,
device_y=None,
device_z=None,
)
super().__init__(parent=parent, config=config, theme_update=True, **kwargs)
self._image_config = config
@@ -314,12 +314,12 @@ class Heatmap(ImageBase):
@SafeSlot(popup_error=True)
def plot(
self,
x_name: str,
y_name: str,
z_name: str,
x_entry: None | str = None,
y_entry: None | str = None,
z_entry: None | str = None,
device_x: str,
device_y: str,
device_z: str,
signal_x: None | str = None,
signal_y: None | str = None,
signal_z: None | str = None,
color_map: str | None = "plasma",
validate_bec: bool = True,
interpolation: Literal["linear", "nearest"] | None = None,
@@ -333,12 +333,12 @@ class Heatmap(ImageBase):
Plot the heatmap with the given x, y, and z data.
Args:
x_name (str): The name of the x-axis signal.
y_name (str): The name of the y-axis signal.
z_name (str): The name of the z-axis signal.
x_entry (str | None): The entry for the x-axis signal.
y_entry (str | None): The entry for the y-axis signal.
z_entry (str | None): The entry for the z-axis signal.
device_x (str): The name of the x-axis device signal.
device_y (str): The name of the y-axis device signal.
device_z (str): The name of the z-axis device signal.
signal_x (str | None): The entry for the x-axis device signal.
signal_y (str | None): The entry for the y-axis device signal.
signal_z (str | None): The entry for the z-axis device signal.
color_map (str | None): The color map to use for the heatmap.
validate_bec (bool): Whether to validate the entries against BEC signals.
interpolation (Literal["linear", "nearest"] | None): The interpolation method to use.
@@ -349,13 +349,13 @@ class Heatmap(ImageBase):
reload (bool): Whether to reload the heatmap with new data.
"""
if validate_bec:
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
signal_x = self.entry_validator.validate_signal(device_x, signal_x)
signal_y = self.entry_validator.validate_signal(device_y, signal_y)
signal_z = self.entry_validator.validate_signal(device_z, signal_z)
if x_entry is None or y_entry is None or z_entry is None:
if signal_x is None or signal_y is None or signal_z is None:
raise ValueError("x, y, and z entries must be provided.")
if x_name is None or y_name is None or z_name is None:
if device_x is None or device_y is None or device_z is None:
raise ValueError("x, y, and z names must be provided.")
if interpolation is None:
@@ -374,24 +374,24 @@ class Heatmap(ImageBase):
show_config_label = self._image_config.show_config_label
def _device_key(device: HeatmapDeviceSignal | None) -> tuple[str | None, str | None]:
return (device.name if device else None, device.entry if device else None)
return (device.device if device else None, device.signal if device else None)
prev_cfg = getattr(self, "_image_config", None)
config_changed = False
if prev_cfg and prev_cfg.x_device and prev_cfg.y_device and prev_cfg.z_device:
if prev_cfg and prev_cfg.device_x and prev_cfg.device_y and prev_cfg.device_z:
config_changed = any(
(
_device_key(prev_cfg.x_device) != (x_name, x_entry),
_device_key(prev_cfg.y_device) != (y_name, y_entry),
_device_key(prev_cfg.z_device) != (z_name, z_entry),
_device_key(prev_cfg.device_x) != (device_x, signal_x),
_device_key(prev_cfg.device_y) != (device_y, signal_y),
_device_key(prev_cfg.device_z) != (device_z, signal_z),
)
)
self._image_config = HeatmapConfig(
parent_id=self.gui_id,
x_device=HeatmapDeviceSignal(name=x_name, entry=x_entry),
y_device=HeatmapDeviceSignal(name=y_name, entry=y_entry),
z_device=HeatmapDeviceSignal(name=z_name, entry=z_entry),
device_x=HeatmapDeviceSignal(device=device_x, signal=signal_x),
device_y=HeatmapDeviceSignal(device=device_y, signal=signal_y),
device_z=HeatmapDeviceSignal(device=device_z, signal=signal_z),
color_map=color_map,
color_bar=None,
interpolation=interpolation,
@@ -428,26 +428,26 @@ class Heatmap(ImageBase):
return
# Safely get device names (might be None if not yet configured)
x_device = self._image_config.x_device
y_device = self._image_config.y_device
z_device = self._image_config.z_device
device_x = self._image_config.device_x
device_y = self._image_config.device_y
device_z = self._image_config.device_z
x_name = x_device.name if x_device else None
y_name = y_device.name if y_device else None
z_name = z_device.name if z_device else None
device_x_name = device_x.device if device_x else None
device_y_name = device_y.device if device_y else None
device_z_name = device_z.device if device_z else None
if x_name is not None:
self.x_label = x_name # type: ignore
x_dev = self.dev.get(x_name)
if device_x_name is not None:
self.x_label = device_x_name # type: ignore
x_dev = self.dev.get(device_x_name)
if x_dev and hasattr(x_dev, "egu"):
self.x_label_units = x_dev.egu()
if y_name is not None:
self.y_label = y_name # type: ignore
y_dev = self.dev.get(y_name)
if device_y_name is not None:
self.y_label = device_y_name # type: ignore
y_dev = self.dev.get(device_y_name)
if y_dev and hasattr(y_dev, "egu"):
self.y_label_units = y_dev.egu()
if z_name is not None:
self.title = z_name
if device_z_name is not None:
self.title = device_z_name
def _init_toolbar_heatmap(self):
"""
@@ -572,23 +572,23 @@ class Heatmap(ImageBase):
if self._image_config is None:
return
try:
x_name = self._image_config.x_device.name
x_entry = self._image_config.x_device.entry
y_name = self._image_config.y_device.name
y_entry = self._image_config.y_device.entry
z_name = self._image_config.z_device.name
z_entry = self._image_config.z_device.entry
device_x = self._image_config.device_x.device
signal_x = self._image_config.device_x.signal
device_y = self._image_config.device_y.device
signal_y = self._image_config.device_y.signal
device_z = self._image_config.device_z.device
signal_z = self._image_config.device_z.signal
except AttributeError:
return
if access_key == "val":
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
y_data = data.get(y_name, {}).get(y_entry, {}).get(access_key, None)
z_data = data.get(z_name, {}).get(z_entry, {}).get(access_key, None)
x_data = data.get(device_x, {}).get(signal_x, {}).get(access_key, None)
y_data = data.get(device_y, {}).get(signal_y, {}).get(access_key, None)
z_data = data.get(device_z, {}).get(signal_z, {}).get(access_key, None)
else:
x_data = data.get(x_name, {}).get(x_entry, {}).read().get("value", None)
y_data = data.get(y_name, {}).get(y_entry, {}).read().get("value", None)
z_data = data.get(z_name, {}).get(z_entry, {}).read().get("value", None)
x_data = data.get(device_x, {}).get(signal_x, {}).read().get("value", None)
y_data = data.get(device_y, {}).get(signal_y, {}).read().get("value", None)
z_data = data.get(device_z, {}).get(signal_z, {}).read().get("value", None)
if not isinstance(x_data, list):
x_data = x_data.tolist() if isinstance(x_data, np.ndarray) else None
@@ -839,7 +839,6 @@ class Heatmap(ImageBase):
x_data (np.ndarray): The x data.
y_data (np.ndarray): The y data.
z_data (np.ndarray): The z data.
msg (messages.ScanStatusMessage): The scan status message.
Returns:
tuple[np.ndarray, QTransform]: The image data and the QTransform.
@@ -854,7 +853,7 @@ class Heatmap(ImageBase):
if len(z_data) < 4:
# LinearNDInterpolator requires at least 4 points to interpolate
return None, None
return self.get_step_scan_image(x_data, y_data, z_data, msg)
return self.get_step_scan_image(x_data, y_data, z_data)
def _is_grid_scan_supported(self, msg: messages.ScanStatusMessage) -> bool:
"""Check if the scan can use optimized grid_scan rendering.
@@ -871,11 +870,11 @@ class Heatmap(ImageBase):
if msg.scan_name != "grid_scan" or self._image_config.enforce_interpolation:
return False
device_x = self._image_config.x_device.entry
device_y = self._image_config.y_device.entry
signal_x = self._image_config.device_x.signal
signal_y = self._image_config.device_y.signal
return (
device_x in msg.request_inputs["arg_bundle"]
and device_y in msg.request_inputs["arg_bundle"]
signal_x in msg.request_inputs["arg_bundle"]
and signal_y in msg.request_inputs["arg_bundle"]
)
def get_grid_scan_image(
@@ -893,9 +892,9 @@ class Heatmap(ImageBase):
args = self.arg_bundle_to_dict(4, msg.request_inputs["arg_bundle"])
x_entry = self._image_config.x_device.entry
y_entry = self._image_config.y_device.entry
shape = (args[x_entry][-1], args[y_entry][-1])
signal_x = self._image_config.device_x.signal
signal_y = self._image_config.device_y.signal
shape = (args[signal_x][-1], args[signal_y][-1])
data = self.main_image.raw_data
@@ -925,8 +924,8 @@ class Heatmap(ImageBase):
return origin + np.linspace(start, stop, npts)
return np.linspace(start, stop, npts)
x_levels = _axis_levels(x_entry, shape[0])
y_levels = _axis_levels(y_entry, shape[1])
x_levels = _axis_levels(signal_x, shape[0])
y_levels = _axis_levels(signal_y, shape[1])
pixel_size_x = (
float(x_levels[-1] - x_levels[0]) / max(shape[0] - 1, 1) if shape[0] > 1 else 1.0
@@ -949,7 +948,7 @@ class Heatmap(ImageBase):
if snaked and (slow_i % 2 == 1):
fast_i = args[fast_entry][-1] - 1 - fast_i
if x_entry == fast_entry:
if signal_x == fast_entry:
x_i, y_i = fast_i, slow_i
else:
x_i, y_i = slow_i, fast_i
@@ -959,11 +958,7 @@ class Heatmap(ImageBase):
return data, transform
def get_step_scan_image(
self,
x_data: list[float],
y_data: list[float],
z_data: list[float],
msg: messages.ScanStatusMessage,
self, x_data: list[float], y_data: list[float], z_data: list[float]
) -> tuple[np.ndarray, QTransform]:
"""
Get the image data for an arbitrary step scan.
@@ -972,7 +967,6 @@ class Heatmap(ImageBase):
x_data (list[float]): The x data.
y_data (list[float]): The y data.
z_data (list[float]): The z data.
msg (messages.ScanStatusMessage): The scan status message.
Returns:
tuple[np.ndarray, QTransform]: The image data and the QTransform.
@@ -1033,7 +1027,7 @@ class Heatmap(ImageBase):
to avoid recalculating the grid for the same scan.
Args:
_scan_id (str): The scan ID. Needed for caching but not used in the function.
positions: positions of the data points.
Returns:
tuple[np.ndarray, np.ndarray, QTransform]: The grid x and y coordinates and the QTransform.
@@ -1108,11 +1102,13 @@ class Heatmap(ImageBase):
return max(1, width_pixels), max(1, height_pixels)
def arg_bundle_to_dict(self, bundle_size: int, args: list) -> dict:
@staticmethod
def arg_bundle_to_dict(bundle_size: int, args: list) -> dict:
"""
Convert the argument bundle to a dictionary.
Args:
bundle_size (int): The size of each argument bundle.
args (list): The argument bundle.
Returns:
@@ -1160,14 +1156,14 @@ class Heatmap(ImageBase):
################################################################################
@SafeProperty(str)
def x_device_name(self) -> str:
def device_x(self) -> str:
"""Device name for the X axis."""
if self._image_config.x_device is None:
if self._image_config.device_x is None:
return ""
return self._image_config.x_device.name or ""
return self._image_config.device_x.device or ""
@x_device_name.setter
def x_device_name(self, device_name: str) -> None:
@device_x.setter
def device_x(self, device_name: str) -> None:
"""
Set the X device name.
@@ -1179,27 +1175,27 @@ class Heatmap(ImageBase):
# Get current entry or validate
if device_name:
try:
entry = self.entry_validator.validate_signal(device_name, None)
self._image_config.x_device = HeatmapDeviceSignal(name=device_name, entry=entry)
self.property_changed.emit("x_device_name", device_name)
signal = self.entry_validator.validate_signal(device_name, None)
self._image_config.device_x = HeatmapDeviceSignal(device=device_name, signal=signal)
self.property_changed.emit("device_x", device_name)
self.update_labels() # Update axis labels
self._try_auto_plot()
except Exception:
pass # Silently fail if device is not available yet
else:
self._image_config.x_device = None
self.property_changed.emit("x_device_name", "")
self._image_config.device_x = None
self.property_changed.emit("device_x", "")
self.update_labels() # Clear axis labels
@SafeProperty(str)
def x_device_entry(self) -> str:
def signal_x(self) -> str:
"""Signal entry for the X axis device."""
if self._image_config.x_device is None:
if self._image_config.device_x is None:
return ""
return self._image_config.x_device.entry or ""
return self._image_config.device_x.signal or ""
@x_device_entry.setter
def x_device_entry(self, entry: str) -> None:
@signal_x.setter
def signal_x(self, entry: str) -> None:
"""
Set the X device entry.
@@ -1209,32 +1205,32 @@ class Heatmap(ImageBase):
if not entry:
return
if self._image_config.x_device is None:
logger.warning("Cannot set x_device_entry without x_device_name set first.")
if self._image_config.device_x is None:
logger.warning("Cannot set signal_x without device_x set first.")
return
device_name = self._image_config.x_device.name
device_name = self._image_config.device_x.device
try:
# Validate the entry for this device
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._image_config.x_device = HeatmapDeviceSignal(
name=device_name, entry=validated_entry
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._image_config.device_x = HeatmapDeviceSignal(
device=device_name, signal=validated_signal
)
self.property_changed.emit("x_device_entry", validated_entry)
self.property_changed.emit("signal_x", validated_signal)
self.update_labels() # Update axis labels
self._try_auto_plot()
except Exception:
pass # Silently fail if validation fails
@SafeProperty(str)
def y_device_name(self) -> str:
def device_y(self) -> str:
"""Device name for the Y axis."""
if self._image_config.y_device is None:
if self._image_config.device_y is None:
return ""
return self._image_config.y_device.name or ""
return self._image_config.device_y.device or ""
@y_device_name.setter
def y_device_name(self, device_name: str) -> None:
@device_y.setter
def device_y(self, device_name: str) -> None:
"""
Set the Y device name.
@@ -1246,27 +1242,27 @@ class Heatmap(ImageBase):
# Get current entry or validate
if device_name:
try:
entry = self.entry_validator.validate_signal(device_name, None)
self._image_config.y_device = HeatmapDeviceSignal(name=device_name, entry=entry)
self.property_changed.emit("y_device_name", device_name)
signal = self.entry_validator.validate_signal(device_name, None)
self._image_config.device_y = HeatmapDeviceSignal(device=device_name, signal=signal)
self.property_changed.emit("device_y", device_name)
self.update_labels() # Update axis labels
self._try_auto_plot()
except Exception:
pass # Silently fail if device is not available yet
else:
self._image_config.y_device = None
self.property_changed.emit("y_device_name", "")
self._image_config.device_y = None
self.property_changed.emit("device_y", "")
self.update_labels() # Clear axis labels
@SafeProperty(str)
def y_device_entry(self) -> str:
def signal_y(self) -> str:
"""Signal entry for the Y axis device."""
if self._image_config.y_device is None:
if self._image_config.device_y is None:
return ""
return self._image_config.y_device.entry or ""
return self._image_config.device_y.signal or ""
@y_device_entry.setter
def y_device_entry(self, entry: str) -> None:
@signal_y.setter
def signal_y(self, entry: str) -> None:
"""
Set the Y device entry.
@@ -1276,18 +1272,18 @@ class Heatmap(ImageBase):
if not entry:
return
if self._image_config.y_device is None:
logger.warning("Cannot set y_device_entry without y_device_name set first.")
if self._image_config.device_y is None:
logger.warning("Cannot set signal_y without device_y set first.")
return
device_name = self._image_config.y_device.name
device_name = self._image_config.device_y.device
try:
# Validate the entry for this device
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._image_config.y_device = HeatmapDeviceSignal(
name=device_name, entry=validated_entry
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._image_config.device_y = HeatmapDeviceSignal(
device=device_name, signal=validated_signal
)
self.property_changed.emit("y_device_entry", validated_entry)
self.property_changed.emit("signal_y", validated_signal)
self.update_labels() # Update axis labels
self._try_auto_plot()
except Exception as e:
@@ -1295,14 +1291,14 @@ class Heatmap(ImageBase):
pass # Silently fail if validation fails
@SafeProperty(str)
def z_device_name(self) -> str:
def device_z(self) -> str:
"""Device name for the Z (color) axis."""
if self._image_config.z_device is None:
if self._image_config.device_z is None:
return ""
return self._image_config.z_device.name or ""
return self._image_config.device_z.device or ""
@z_device_name.setter
def z_device_name(self, device_name: str) -> None:
@device_z.setter
def device_z(self, device_name: str) -> None:
"""
Set the Z device name.
@@ -1314,28 +1310,28 @@ class Heatmap(ImageBase):
# Get current entry or validate
if device_name:
try:
entry = self.entry_validator.validate_signal(device_name, None)
self._image_config.z_device = HeatmapDeviceSignal(name=device_name, entry=entry)
self.property_changed.emit("z_device_name", device_name)
signal = self.entry_validator.validate_signal(device_name, None)
self._image_config.device_z = HeatmapDeviceSignal(device=device_name, signal=signal)
self.property_changed.emit("device_z", device_name)
self.update_labels() # Update axis labels (title)
self._try_auto_plot()
except Exception as e:
logger.debug(f"Z device name validation failed: {e}")
pass # Silently fail if device is not available yet
else:
self._image_config.z_device = None
self.property_changed.emit("z_device_name", "")
self._image_config.device_z = None
self.property_changed.emit("device_z", "")
self.update_labels() # Clear axis labels
@SafeProperty(str)
def z_device_entry(self) -> str:
def signal_z(self) -> str:
"""Signal entry for the Z (color) axis device."""
if self._image_config.z_device is None:
if self._image_config.device_z is None:
return ""
return self._image_config.z_device.entry or ""
return self._image_config.device_z.signal or ""
@z_device_entry.setter
def z_device_entry(self, entry: str) -> None:
@signal_z.setter
def signal_z(self, entry: str) -> None:
"""
Set the Z device entry.
@@ -1345,18 +1341,18 @@ class Heatmap(ImageBase):
if not entry:
return
if self._image_config.z_device is None:
logger.warning("Cannot set z_device_entry without z_device_name set first.")
if self._image_config.device_z is None:
logger.warning("Cannot set signal_z without device_z set first.")
return
device_name = self._image_config.z_device.name
device_name = self._image_config.device_z.device
try:
# Validate the entry for this device
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._image_config.z_device = HeatmapDeviceSignal(
name=device_name, entry=validated_entry
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._image_config.device_z = HeatmapDeviceSignal(
device=device_name, signal=validated_signal
)
self.property_changed.emit("z_device_entry", validated_entry)
self.property_changed.emit("signal_z", validated_signal)
self.update_labels() # Update axis labels (title)
self._try_auto_plot()
except Exception as e:
@@ -1368,25 +1364,25 @@ class Heatmap(ImageBase):
Attempt to automatically call plot() if all three devices are set.
Similar to waveform's approach but requires all three devices.
"""
has_x = self._image_config.x_device is not None
has_y = self._image_config.y_device is not None
has_z = self._image_config.z_device is not None
has_x = self._image_config.device_x is not None
has_y = self._image_config.device_y is not None
has_z = self._image_config.device_z is not None
if has_x and has_y and has_z:
x_name = self._image_config.x_device.name
x_entry = self._image_config.x_device.entry
y_name = self._image_config.y_device.name
y_entry = self._image_config.y_device.entry
z_name = self._image_config.z_device.name
z_entry = self._image_config.z_device.entry
device_x = self._image_config.device_x.device
signal_x = self._image_config.device_x.signal
device_y = self._image_config.device_y.device
signal_y = self._image_config.device_y.signal
device_z = self._image_config.device_z.device
signal_z = self._image_config.device_z.signal
try:
self.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
device_x=device_x,
device_y=device_y,
device_z=device_z,
signal_x=signal_x,
signal_y=signal_y,
signal_z=signal_z,
validate_bec=False, # Don't validate - entries already validated
)
except Exception as e:
@@ -1533,6 +1529,6 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
heatmap = Heatmap()
heatmap.plot(x_name="samx", y_name="samy", z_name="bpm4i", oversampling_factor=5.0)
heatmap.plot(device_x="samx", device_y="samy", device_z="bpm4i", oversampling_factor=5.0)
heatmap.show()
sys.exit(app.exec_())

View File

@@ -48,7 +48,7 @@ class HeatmapSettings(SettingWidget):
if popup is False:
self.ui.button_apply.clicked.connect(self.accept_changes)
self.ui.x_name.setFocus()
self.ui.device_x.setFocus()
@SafeSlot()
def fetch_all_properties(self):
@@ -62,44 +62,44 @@ class HeatmapSettings(SettingWidget):
color_map = getattr(self.target_widget, "color_map", None)
# Default values for device properties
x_name, x_entry = None, None
y_name, y_entry = None, None
z_name, z_entry = None, None
device_x, signal_x = None, None
device_y, signal_y = None, None
device_z, signal_z = None, None
# Safely access device properties
if hasattr(self.target_widget, "_image_config") and self.target_widget._image_config:
config = self.target_widget._image_config
if hasattr(config, "x_device") and config.x_device:
x_name = getattr(config.x_device, "name", None)
x_entry = getattr(config.x_device, "entry", None)
if hasattr(config, "device_x") and config.device_x:
device_x = getattr(config.device_x, "device", None)
signal_x = getattr(config.device_x, "signal", None)
if hasattr(config, "y_device") and config.y_device:
y_name = getattr(config.y_device, "name", None)
y_entry = getattr(config.y_device, "entry", None)
if hasattr(config, "device_y") and config.device_y:
device_y = getattr(config.device_y, "device", None)
signal_y = getattr(config.device_y, "signal", None)
if hasattr(config, "z_device") and config.z_device:
z_name = getattr(config.z_device, "name", None)
z_entry = getattr(config.z_device, "entry", None)
if hasattr(config, "device_z") and config.device_z:
device_z = getattr(config.device_z, "device", None)
signal_z = getattr(config.device_z, "signal", None)
# Apply the properties to the settings widget
if hasattr(self.ui, "color_map"):
self.ui.color_map.colormap = color_map
if hasattr(self.ui, "x_name"):
self.ui.x_name.set_device(x_name)
if hasattr(self.ui, "x_entry") and x_entry is not None:
self.ui.x_entry.set_to_obj_name(x_entry)
if hasattr(self.ui, "device_x"):
self.ui.device_x.set_device(device_x)
if hasattr(self.ui, "signal_x") and signal_x is not None:
self.ui.signal_x.set_to_obj_name(signal_x)
if hasattr(self.ui, "y_name"):
self.ui.y_name.set_device(y_name)
if hasattr(self.ui, "y_entry") and y_entry is not None:
self.ui.y_entry.set_to_obj_name(y_entry)
if hasattr(self.ui, "device_y"):
self.ui.device_y.set_device(device_y)
if hasattr(self.ui, "signal_y") and signal_y is not None:
self.ui.signal_y.set_to_obj_name(signal_y)
if hasattr(self.ui, "z_name"):
self.ui.z_name.set_device(z_name)
if hasattr(self.ui, "z_entry") and z_entry is not None:
self.ui.z_entry.set_to_obj_name(z_entry)
if hasattr(self.ui, "device_z"):
self.ui.device_z.set_device(device_z)
if hasattr(self.ui, "signal_z") and signal_z is not None:
self.ui.signal_z.set_to_obj_name(signal_z)
if hasattr(self.ui, "interpolation"):
self.ui.interpolation.setCurrentText(
@@ -119,12 +119,12 @@ class HeatmapSettings(SettingWidget):
"""
Apply all properties from the settings widget to the target widget.
"""
x_name = self.ui.x_name.currentText()
x_entry = self.ui.x_entry.get_signal_name()
y_name = self.ui.y_name.currentText()
y_entry = self.ui.y_entry.get_signal_name()
z_name = self.ui.z_name.currentText()
z_entry = self.ui.z_entry.get_signal_name()
device_x = self.ui.device_x.currentText()
signal_x = self.ui.signal_x.get_signal_name()
device_y = self.ui.device_y.currentText()
signal_y = self.ui.signal_y.get_signal_name()
device_z = self.ui.device_z.currentText()
signal_z = self.ui.signal_z.get_signal_name()
validate_bec = self.ui.validate_bec.checked
color_map = self.ui.color_map.colormap
interpolation = self.ui.interpolation.currentText()
@@ -132,12 +132,12 @@ class HeatmapSettings(SettingWidget):
enforce_interpolation = self.ui.enforce_interpolation.isChecked()
self.target_widget.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
device_x=device_x,
device_y=device_y,
device_z=device_z,
signal_x=signal_x,
signal_y=signal_y,
signal_z=signal_z,
color_map=color_map,
validate_bec=validate_bec,
interpolation=interpolation,
@@ -147,17 +147,17 @@ class HeatmapSettings(SettingWidget):
)
def cleanup(self):
self.ui.x_name.close()
self.ui.x_name.deleteLater()
self.ui.x_entry.close()
self.ui.x_entry.deleteLater()
self.ui.y_name.close()
self.ui.y_name.deleteLater()
self.ui.y_entry.close()
self.ui.y_entry.deleteLater()
self.ui.z_name.close()
self.ui.z_name.deleteLater()
self.ui.z_entry.close()
self.ui.z_entry.deleteLater()
self.ui.device_x.close()
self.ui.device_x.deleteLater()
self.ui.signal_x.close()
self.ui.signal_x.deleteLater()
self.ui.device_y.close()
self.ui.device_y.deleteLater()
self.ui.signal_y.close()
self.ui.signal_y.deleteLater()
self.ui.device_z.close()
self.ui.device_z.deleteLater()
self.ui.signal_z.close()
self.ui.signal_z.deleteLater()
self.ui.interpolation.close()
self.ui.interpolation.deleteLater()

View File

@@ -196,7 +196,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="x_name">
<widget class="DeviceComboBox" name="device_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -206,7 +206,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="x_entry">
<widget class="SignalComboBox" name="signal_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -236,7 +236,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="y_name">
<widget class="DeviceComboBox" name="device_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -246,7 +246,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="y_entry">
<widget class="SignalComboBox" name="signal_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -276,7 +276,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="z_name">
<widget class="DeviceComboBox" name="device_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -286,7 +286,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="z_entry">
<widget class="SignalComboBox" name="signal_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -322,21 +322,21 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>x_name</tabstop>
<tabstop>y_name</tabstop>
<tabstop>z_name</tabstop>
<tabstop>x_entry</tabstop>
<tabstop>y_entry</tabstop>
<tabstop>z_entry</tabstop>
<tabstop>device_x</tabstop>
<tabstop>device_y</tabstop>
<tabstop>device_z</tabstop>
<tabstop>signal_x</tabstop>
<tabstop>signal_y</tabstop>
<tabstop>signal_z</tabstop>
<tabstop>interpolation</tabstop>
<tabstop>oversampling_factor</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>device_reset()</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -350,9 +350,9 @@
</hints>
</connection>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -366,9 +366,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>device_reset()</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -382,9 +382,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -398,9 +398,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>device_reset()</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -414,9 +414,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">

View File

@@ -69,7 +69,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="x_name">
<widget class="DeviceComboBox" name="device_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -79,7 +79,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="x_entry">
<widget class="SignalComboBox" name="signal_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -109,7 +109,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="y_name">
<widget class="DeviceComboBox" name="device_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -119,7 +119,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="y_entry">
<widget class="SignalComboBox" name="signal_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -142,7 +142,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="z_name">
<widget class="DeviceComboBox" name="device_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -152,7 +152,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="z_entry">
<widget class="SignalComboBox" name="signal_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -264,20 +264,20 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>x_name</tabstop>
<tabstop>y_name</tabstop>
<tabstop>z_name</tabstop>
<tabstop>device_x</tabstop>
<tabstop>device_y</tabstop>
<tabstop>device_z</tabstop>
<tabstop>button_apply</tabstop>
<tabstop>x_entry</tabstop>
<tabstop>y_entry</tabstop>
<tabstop>z_entry</tabstop>
<tabstop>signal_x</tabstop>
<tabstop>signal_y</tabstop>
<tabstop>signal_z</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>device_reset()</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -291,9 +291,9 @@
</hints>
</connection>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -307,9 +307,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>device_reset()</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -323,9 +323,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -339,9 +339,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>device_reset()</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -355,9 +355,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">

View File

@@ -42,8 +42,8 @@ class ImageConfig(ConnectionConfig):
class ImageLayerConfig(BaseModel):
device_name: str = Field("", description="The device name to monitor.")
device_entry: str = Field("", description="The signal/entry name to monitor on the device.")
device: str = Field("", description="The device name to monitor.")
signal: str = Field("", description="The signal/entry name to monitor on the device.")
monitor_type: Literal["1d", "2d"] | None = Field(None, description="The type of monitor.")
source: Literal["device_monitor_1d", "device_monitor_2d"] | None = Field(
None, description="The source of the image data."
@@ -80,10 +80,10 @@ class Image(ImageBase):
"autorange.setter",
"autorange_mode",
"autorange_mode.setter",
"device_name",
"device_name.setter",
"device_entry",
"device_entry.setter",
"device",
"device.setter",
"signal",
"signal.setter",
"enable_colorbar",
"enable_simple_colorbar",
"enable_simple_colorbar.setter",
@@ -206,19 +206,19 @@ class Image(ImageBase):
signal_text = device_selection.signal_combo_box.currentText()
if not device:
self.device_name = ""
self.device = ""
return
if not device_selection.device_combo_box.is_valid_input:
return
if not device_selection.signal_combo_box.is_valid_input:
if self._config.device_entry:
self.device_entry = ""
if device != self._config.device_name:
self.device_name = device
if self._config.signal:
self.signal = ""
if device != self._config.device:
self.device = device
return
if device == self._config.device_name and signal_text == self._config.device_entry:
if device == self._config.device and signal_text == self._config.signal:
return
# Get the signal config stored in the combobox
@@ -235,8 +235,8 @@ class Image(ImageBase):
# Store signal config and set properties which will trigger the connection
self._signal_configs["main"] = signal_config
self.device_name = device
self.device_entry = signal_text
self.device = device
self.signal = signal_text
finally:
self._device_selection_updating = False
@@ -244,55 +244,53 @@ class Image(ImageBase):
# Data Acquisition
@SafeProperty(str, auto_emit=True)
def device_name(self) -> str:
def device(self) -> str:
"""
The name of the device to monitor for image data.
"""
return self._config.device_name
return self._config.device
@device_name.setter
def device_name(self, value: str):
@device.setter
def device(self, value: str):
"""
Set the device name for the image. This should be used together with device_entry.
When both device_name and device_entry are set, the widget connects to that device signal.
Set the device name for the image. This should be used together with signal.
When both device and signal are set, the widget connects to that device signal.
Args:
value(str): The name of the device to monitor.
"""
if not value:
# Clear the monitor if empty device name
if self._config.device_name:
if self._config.device:
self._disconnect_current_monitor()
self._config.device_name = ""
self._config.device_entry = ""
self._config.device = ""
self._config.signal = ""
self._signal_configs.pop("main", None)
self._set_connection_status("disconnected")
return
old_device = self._config.device_name
self._config.device_name = value
old_device = self._config.device
self._config.device = value
# If we have a device_entry, reconnect with the new device
if self._config.device_entry:
# If we have a signal, reconnect with the new device
if self._config.signal:
# Try to get fresh signal config for the new device
try:
device_obj = self.dev[value]
# Try to get signal config for the current entry
if self._config.device_entry in device_obj._info.get("signals", {}):
self._signal_configs["main"] = device_obj._info["signals"][
self._config.device_entry
]
if self._config.signal in device_obj._info.get("signals", {}):
self._signal_configs["main"] = device_obj._info["signals"][self._config.signal]
self._setup_connection()
else:
# Signal doesn't exist on new device
logger.warning(
f"Signal '{self._config.device_entry}' doesn't exist on device '{value}'"
f"Signal '{self._config.signal}' doesn't exist on device '{value}'"
)
self._disconnect_current_monitor()
self._config.device_entry = ""
self._config.signal = ""
self._signal_configs.pop("main", None)
self._set_connection_status(
"error", f"Signal '{self._config.device_entry}' doesn't exist"
"error", f"Signal '{self._config.signal}' doesn't exist"
)
except (KeyError, AttributeError):
# Device doesn't exist
@@ -304,40 +302,40 @@ class Image(ImageBase):
# Toolbar sync happens via SafeProperty auto_emit property_changed handling.
@SafeProperty(str, auto_emit=True)
def device_entry(self) -> str:
def signal(self) -> str:
"""
The signal/entry name to monitor on the device.
"""
return self._config.device_entry
return self._config.signal
@device_entry.setter
def device_entry(self, value: str):
@signal.setter
def signal(self, value: str):
"""
Set the device entry (signal) for the image. This should be used together with device_name.
Set the device signal for the image. This should be used together with device.
When set, it will connect to updates from that device signal.
Args:
value(str): The signal name to monitor.
"""
if not value:
if self._config.device_entry:
if self._config.signal:
self._disconnect_current_monitor()
self._config.device_entry = ""
self._config.signal = ""
self._signal_configs.pop("main", None)
self._set_connection_status("disconnected")
return
self._config.device_entry = value
self._config.signal = value
# If we have a device_name, try to connect
if self._config.device_name:
# If we have a device, try to connect
if self._config.device:
try:
device_obj = self.dev[self._config.device_name]
device_obj = self.dev[self._config.device]
signal_config = device_obj._info["signals"].get(value)
if not isinstance(signal_config, dict) or not signal_config.get("signal_class"):
logger.warning(
f"Could not find valid configuration for signal '{value}' "
f"on device '{self._config.device_name}'."
f"on device '{self._config.device}'."
)
self._signal_configs.pop("main", None)
self._set_connection_status("error", f"Signal '{value}' not found")
@@ -347,14 +345,14 @@ class Image(ImageBase):
self._setup_connection()
except (KeyError, AttributeError):
logger.warning(
f"Could not find signal '{value}' on device '{self._config.device_name}'."
f"Could not find signal '{value}' on device '{self._config.device}'."
)
# Remove signal config if it can't be fetched
self._signal_configs.pop("main", None)
self._set_connection_status("error", f"Signal '{value}' not found")
else:
logger.debug(f"device_entry setter: No device set yet for signal '{value}'")
logger.debug(f"signal setter: No device set yet for signal '{value}'")
@property
def main_image(self) -> ImageItem:
@@ -363,17 +361,17 @@ class Image(ImageBase):
def _setup_connection(self):
"""
Internal method to setup connection based on current device_name, device_entry, and signal_config.
Internal method to setup connection based on current device, signal, and signal_config.
"""
if not self._config.device_name or not self._config.device_entry:
logger.warning("Cannot setup connection without both device_name and device_entry")
if not self._config.device or not self._config.signal:
logger.warning("Cannot setup connection without both device and signal")
self._set_connection_status("disconnected")
return
signal_config = self._signal_configs.get("main")
if not signal_config:
logger.warning(
f"Cannot setup connection for {self._config.device_name}.{self._config.device_entry} without signal_config"
f"Cannot setup connection for {self._config.device}.{self._config.signal} without signal_config"
)
self._set_connection_status("error", "Missing signal config")
return
@@ -387,7 +385,7 @@ class Image(ImageBase):
if signal_class not in supported_classes:
logger.warning(
f"Signal '{self._config.device_name}.{self._config.device_entry}' has unsupported signal class '{signal_class}'. "
f"Signal '{self._config.device}.{self._config.signal}' has unsupported signal class '{signal_class}'. "
f"Supported classes: {supported_classes}"
)
self._set_connection_status("error", f"Unsupported signal class '{signal_class}'")
@@ -399,7 +397,7 @@ class Image(ImageBase):
if ndim is None:
logger.warning(
f"Signal '{self._config.device_name}.{self._config.device_entry}' does not have a valid 'ndim' in its signal_info."
f"Signal '{self._config.device}.{self._config.signal}' does not have a valid 'ndim' in its signal_info."
)
self._set_connection_status("error", "Missing ndim in signal_info")
return
@@ -414,14 +412,12 @@ class Image(ImageBase):
if signal_class == "PreviewSignal":
self.bec_dispatcher.connect_slot(
self.on_image_update_1d,
MessageEndpoints.device_preview(
self._config.device_name, self._config.device_entry
),
MessageEndpoints.device_preview(self._config.device, self._config.signal),
)
elif signal_class in self.SUPPORTED_SIGNALS:
self.async_update = True
config.async_signal_name = signal_config.get(
"obj_name", f"{self._config.device_name}_{self._config.device_entry}"
"obj_name", f"{self._config.device}_{self._config.signal}"
)
self._setup_async_image(self.scan_id)
elif ndim == 2:
@@ -430,26 +426,24 @@ class Image(ImageBase):
if signal_class == "PreviewSignal":
self.bec_dispatcher.connect_slot(
self.on_image_update_2d,
MessageEndpoints.device_preview(
self._config.device_name, self._config.device_entry
),
MessageEndpoints.device_preview(self._config.device, self._config.signal),
)
elif signal_class in self.SUPPORTED_SIGNALS:
self.async_update = True
config.async_signal_name = signal_config.get(
"obj_name", f"{self._config.device_name}_{self._config.device_entry}"
"obj_name", f"{self._config.device}_{self._config.signal}"
)
self._setup_async_image(self.scan_id)
else:
logger.warning(
f"Unsupported ndim '{ndim}' for monitor '{self._config.device_name}.{self._config.device_entry}'."
f"Unsupported ndim '{ndim}' for monitor '{self._config.device}.{self._config.signal}'."
)
self._set_connection_status("error", f"Unsupported ndim '{ndim}'")
return
self._set_connection_status("connected")
logger.info(
f"Connected to {self._config.device_name}.{self._config.device_entry} with type {config.monitor_type}"
f"Connected to {self._config.device}.{self._config.signal} with type {config.monitor_type}"
)
self._autorange_on_next_update = True
@@ -457,13 +451,13 @@ class Image(ImageBase):
"""
Internal method to disconnect the current monitor subscriptions.
"""
if not self._config.device_name or not self._config.device_entry:
if not self._config.device or not self._config.signal:
return
config = self.subscriptions["main"]
if self.async_update:
async_signal_name = config.async_signal_name or self._config.device_entry
async_signal_name = config.async_signal_name or self._config.signal
ids_to_check = [self.scan_id, self.old_scan_id]
if config.source == "device_monitor_1d":
@@ -473,11 +467,11 @@ class Image(ImageBase):
self.bec_dispatcher.disconnect_slot(
self.on_image_update_1d,
MessageEndpoints.device_async_signal(
scan_id, self._config.device_name, async_signal_name
scan_id, self._config.device, async_signal_name
),
)
logger.info(
f"Disconnecting 1d update ScanID:{scan_id}, Device Name:{self._config.device_name},Device Entry:{async_signal_name}"
f"Disconnecting 1d update ScanID:{scan_id}, Device Name:{self._config.device},Device Entry:{async_signal_name}"
)
elif config.source == "device_monitor_2d":
for scan_id in ids_to_check:
@@ -486,33 +480,29 @@ class Image(ImageBase):
self.bec_dispatcher.disconnect_slot(
self.on_image_update_2d,
MessageEndpoints.device_async_signal(
scan_id, self._config.device_name, async_signal_name
scan_id, self._config.device, async_signal_name
),
)
logger.info(
f"Disconnecting 2d update ScanID:{scan_id}, Device Name:{self._config.device_name},Device Entry:{async_signal_name}"
f"Disconnecting 2d update ScanID:{scan_id}, Device Name:{self._config.device},Device Entry:{async_signal_name}"
)
else:
if config.source == "device_monitor_1d":
self.bec_dispatcher.disconnect_slot(
self.on_image_update_1d,
MessageEndpoints.device_preview(
self._config.device_name, self._config.device_entry
),
MessageEndpoints.device_preview(self._config.device, self._config.signal),
)
logger.info(
f"Disconnecting preview 1d update Device Name:{self._config.device_name}, Device Entry:{self._config.device_entry}"
f"Disconnecting preview 1d update Device Name:{self._config.device}, Device Entry:{self._config.signal}"
)
elif config.source == "device_monitor_2d":
self.bec_dispatcher.disconnect_slot(
self.on_image_update_2d,
MessageEndpoints.device_preview(
self._config.device_name, self._config.device_entry
),
MessageEndpoints.device_preview(self._config.device, self._config.signal),
)
logger.info(
f"Disconnecting preview 2d update Device Name:{self._config.device_name}, Device Entry:{self._config.device_entry}"
f"Disconnecting preview 2d update Device Name:{self._config.device}, Device Entry:{self._config.signal}"
)
# Reset async state
@@ -526,8 +516,8 @@ class Image(ImageBase):
@SafeSlot(popup_error=True)
def image(
self,
device_name: str | None = None,
device_entry: str | None = None,
device: str | None = None,
signal: str | None = None,
color_map: str | None = None,
color_bar: Literal["simple", "full"] | None = None,
vrange: tuple[int, int] | None = None,
@@ -536,8 +526,8 @@ class Image(ImageBase):
Set the image source and update the image.
Args:
device_name(str|None): The name of the device to monitor. If None or empty string, the current monitor will be disconnected.
device_entry(str|None): The signal/entry name to monitor on the device.
device(str|None): The name of the device to monitor. If None or empty string, the current monitor will be disconnected.
signal(str|None): The signal/entry name to monitor on the device.
color_map(str): The color map to use for the image.
color_bar(str): The type of color bar to use. Options are "simple" or "full".
vrange(tuple): The range of values to use for the color map.
@@ -546,27 +536,27 @@ class Image(ImageBase):
ImageItem: The image object, or None if connection failed.
"""
# Disconnect existing monitor if any
if self._config.device_name and self._config.device_entry:
if self._config.device and self._config.signal:
self._disconnect_current_monitor()
if not device_name or not device_entry:
if device_name or device_entry:
logger.warning("Both device_name and device_entry must be specified")
if not device or not signal:
if device or signal:
logger.warning("Both device and signal must be specified")
else:
logger.info("Disconnecting image monitor")
self.device_name = ""
self.device = ""
return None
# Validate device
self.entry_validator.validate_monitor(device_name)
self.entry_validator.validate_monitor(device)
# Clear old entry first to avoid reconnect attempts on the new device
if self._config.device_entry:
self.device_entry = ""
if self._config.signal:
self.signal = ""
# Set properties to trigger connection
self.device_name = device_name
self.device_entry = device_entry
self.device = device
self.signal = signal
# Apply visual settings
if color_map is not None:
@@ -581,7 +571,7 @@ class Image(ImageBase):
def _sync_device_selection(self):
"""
Synchronize the device and signal comboboxes with the current monitor state.
This ensures the toolbar reflects the device_name and device_entry properties.
This ensures the toolbar reflects the device and signal properties.
"""
try:
device_selection_action = self.toolbar.components.get_action("device_selection")
@@ -593,8 +583,8 @@ class Image(ImageBase):
return
device_selection: DeviceSelection = device_selection_action.widget
target_device = self._config.device_name or ""
target_entry = self._config.device_entry or ""
target_device = self._config.device or ""
target_entry = self._config.signal or ""
# Check if already synced
if (
@@ -605,15 +595,15 @@ class Image(ImageBase):
device_selection.set_device_and_signal(target_device, target_entry)
def _sync_device_entry_from_toolbar(self) -> None:
def _sync_signal_from_toolbar(self) -> None:
"""
Pull the signal selection from the toolbar if it differs from the current device_entry.
This keeps CLI-driven device_name updates in sync with the signal combobox state.
Pull the signal selection from the toolbar if it differs from the current signal.
This keeps CLI-driven device updates in sync with the signal combobox state.
"""
if self._device_selection_updating:
return
if not self._config.device_name:
if not self._config.device:
return
try:
@@ -625,17 +615,17 @@ class Image(ImageBase):
return
device_selection: DeviceSelection = device_selection_action.widget
if device_selection.device_combo_box.currentText() != self._config.device_name:
if device_selection.device_combo_box.currentText() != self._config.device:
return
signal_text = device_selection.signal_combo_box.currentText()
if not signal_text or signal_text == self._config.device_entry:
if not signal_text or signal_text == self._config.signal:
return
signal_config = device_selection.signal_combo_box.get_signal_config()
if not signal_config:
try:
device_obj = self.dev[self._config.device_name]
device_obj = self.dev[self._config.device]
signal_config = device_obj._info["signals"].get(signal_text, {})
except (KeyError, AttributeError):
signal_config = None
@@ -646,7 +636,7 @@ class Image(ImageBase):
self._signal_configs["main"] = signal_config
self._device_selection_updating = True
try:
self.device_entry = signal_text
self.signal = signal_text
finally:
self._device_selection_updating = False
@@ -795,17 +785,17 @@ class Image(ImageBase):
def _get_async_signal_name(self) -> tuple[str, str] | None:
"""
Returns device name and async signal name used for endpoints/messages.
Returns device and async signal names used for endpoints/messages.
Returns:
tuple[str, str] | None: (device_name, async_signal_name) or None if not available.
tuple[str, str] | None: (device, async_signal_name) or None if not available.
"""
if not self._config.device_name or not self._config.device_entry:
if not self._config.device or not self._config.signal:
return None
config = self.subscriptions["main"]
async_signal = config.async_signal_name or self._config.device_entry
return self._config.device_name, async_signal
async_signal = config.async_signal_name or self._config.signal
return self._config.device, async_signal
def _setup_async_image(self, scan_id: str | None):
"""
@@ -823,7 +813,7 @@ class Image(ImageBase):
logger.info("Async image setup skipped because monitor information is incomplete.")
return
device_name, async_signal = async_names
device, async_signal = async_names
if config.monitor_type == "1d":
slot = self.on_image_update_1d
elif config.monitor_type == "2d":
@@ -839,7 +829,7 @@ class Image(ImageBase):
if prev_scan_id is None:
continue
self.bec_dispatcher.disconnect_slot(
slot, MessageEndpoints.device_async_signal(prev_scan_id, device_name, async_signal)
slot, MessageEndpoints.device_async_signal(prev_scan_id, device, async_signal)
)
if scan_id is None:
@@ -848,26 +838,26 @@ class Image(ImageBase):
self.bec_dispatcher.connect_slot(
slot,
MessageEndpoints.device_async_signal(scan_id, device_name, async_signal),
MessageEndpoints.device_async_signal(scan_id, device, async_signal),
from_start=True,
cb_info={"scan_id": scan_id},
)
logger.info(f"Setup async image for {device_name}.{async_signal} and scan {scan_id}.")
logger.info(f"Setup async image for {device}.{async_signal} and scan {scan_id}.")
def disconnect_monitor(self, device_name: str | None = None, device_entry: str | None = None):
def disconnect_monitor(self, device: str | None = None, signal: str | None = None):
"""
Disconnect the monitor from the image update signals, both 1D and 2D.
Args:
device_name(str|None): The name of the device to disconnect. Defaults to current device.
device_entry(str|None): The signal/entry name to disconnect. Defaults to current entry.
device(str|None): The name of the device to disconnect. Defaults to current device.
signal(str|None): The signal/entry name to disconnect. Defaults to current signal.
"""
config = self.subscriptions["main"]
target_device = device_name or self._config.device_name
target_entry = device_entry or self._config.device_entry
target_device = device or self._config.device
target_entry = signal or self._config.signal
if not target_device or not target_entry:
logger.warning("Cannot disconnect monitor without both device_name and device_entry")
logger.warning("Cannot disconnect monitor without both device and signal")
return
if self.async_update:
@@ -1045,10 +1035,10 @@ class Image(ImageBase):
if layer_name not in self.subscriptions:
return
# For the main layer, disconnect current monitor
if layer_name == "main" and self._config.device_name and self._config.device_entry:
if layer_name == "main" and self._config.device and self._config.signal:
self._disconnect_current_monitor()
self._config.device_name = ""
self._config.device_entry = ""
self._config.device = ""
self._config.signal = ""
self._signal_configs.pop("main", None)
def cleanup(self):
@@ -1058,7 +1048,7 @@ class Image(ImageBase):
self.layer_removed.disconnect(self._on_layer_removed)
# Disconnect current monitor
if self._config.device_name and self._config.device_entry:
if self._config.device and self._config.signal:
self._disconnect_current_monitor()
self.subscriptions.clear()

View File

@@ -64,32 +64,32 @@ class DeviceSelection(QWidget):
layout.addWidget(self.device_combo_box, stretch=1)
layout.addWidget(self.signal_combo_box, stretch=1)
def set_device_and_signal(self, device_name: str | None, device_entry: str | None) -> None:
def set_device_and_signal(self, device: str | None, signal: str | None) -> None:
"""Set the displayed device and signal without emitting selection signals."""
device_name = device_name or ""
device_entry = device_entry or ""
device = device or ""
signal = signal or ""
self.device_combo_box.blockSignals(True)
self.signal_combo_box.blockSignals(True)
try:
if device_name:
if device:
# Set device in device_combo_box
index = self.device_combo_box.findText(device_name)
index = self.device_combo_box.findText(device)
if index >= 0:
self.device_combo_box.setCurrentIndex(index)
else:
# Device not found in list, but still set it
self.device_combo_box.setCurrentText(device_name)
self.device_combo_box.setCurrentText(device)
# Only update signal combobox device filter if it's actually changing
# This prevents redundant repopulation which can cause duplicates !!!!
current_device = getattr(self.signal_combo_box, "_device", None)
if current_device != device_name:
self.signal_combo_box.set_device(device_name)
if current_device != device:
self.signal_combo_box.set_device(device)
# Sync signal combobox selection
if device_entry:
if signal:
# Try to find the signal by component_name (which is what's displayed)
found = False
for i in range(self.signal_combo_box.count()):
@@ -99,14 +99,14 @@ class DeviceSelection(QWidget):
# Check if this matches our signal
if config_data:
component_name = config_data.get("component_name", "")
if text == component_name or text == device_entry:
if text == component_name or text == signal:
self.signal_combo_box.setCurrentIndex(i)
found = True
break
if not found:
# Fallback: try to match the device_entry directly
index = self.signal_combo_box.findText(device_entry)
# Fallback: try to match the signal directly
index = self.signal_combo_box.findText(signal)
if index >= 0:
self.signal_combo_box.setCurrentIndex(index)
else:
@@ -187,8 +187,8 @@ class DeviceSelectionConnection(BundleConnection):
self.components = components
self.target_widget = target_widget
self._connected = False
self.register_property_sync("device_name", self._sync_from_device_name)
self.register_property_sync("device_entry", self._sync_from_device_entry)
self.register_property_sync("device", self._sync_from_device)
self.register_property_sync("signal", self._sync_from_signal)
self.register_property_sync("connection_status", self._sync_connection_status)
self.register_property_sync("connection_error", self._sync_connection_status)
@@ -222,26 +222,22 @@ class DeviceSelectionConnection(BundleConnection):
self._connected = False
widget.cleanup()
def _sync_from_device_name(self, _):
def _sync_from_device(self, _):
try:
widget = self._widget()
except Exception:
return
widget.set_device_and_signal(
self.target_widget.device_name, self.target_widget.device_entry
)
self.target_widget._sync_device_entry_from_toolbar()
widget.set_device_and_signal(self.target_widget.device, self.target_widget.signal)
self.target_widget._sync_signal_from_toolbar()
def _sync_from_device_entry(self, _):
def _sync_from_signal(self, _):
try:
widget = self._widget()
except Exception:
return
widget.set_device_and_signal(
self.target_widget.device_name, self.target_widget.device_entry
)
widget.set_device_and_signal(self.target_widget.device, self.target_widget.signal)
def _sync_connection_status(self, _):
try:

View File

@@ -48,14 +48,14 @@ class FilledRectItem(pg.GraphicsObject):
class MotorConfig(BaseModel):
name: str | None = Field(None, description="Motor name.")
device: str | None = Field(None, description="Motor name.")
limits: list[float] | None = Field(None, description="Motor limits.")
# noinspection PyDataclass
class MotorMapConfig(ConnectionConfig):
x_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor X name.")
y_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor Y name.")
device_x: MotorConfig = Field(default_factory=MotorConfig, description="Motor X name.")
device_y: MotorConfig = Field(default_factory=MotorConfig, description="Motor Y name.")
color: str | tuple | None = Field(
(255, 255, 255, 255), description="The color of the last point of current position."
)
@@ -109,10 +109,10 @@ class MotorMap(PlotBase):
"map",
"reset_history",
"get_data",
"x_motor",
"x_motor.setter",
"y_motor",
"y_motor.setter",
"device_x",
"device_x.setter",
"device_y",
"device_y.setter",
]
update_signal = Signal()
@@ -208,7 +208,7 @@ class MotorMap(PlotBase):
return
if motor_x != "" and motor_y != "":
if motor_x != self.config.x_motor.name or motor_y != self.config.y_motor.name:
if motor_x != self.config.device_x.device or motor_y != self.config.device_y.device:
self.map(motor_x, motor_y)
def _add_motor_map_settings(self):
@@ -259,32 +259,32 @@ class MotorMap(PlotBase):
################################################################################
@SafeProperty(str)
def x_motor(self) -> str:
def device_x(self) -> str:
"""Name of the motor shown on the X axis."""
return self.config.x_motor.name or ""
return self.config.device_x.device or ""
@x_motor.setter
def x_motor(self, motor_name: str) -> None:
@device_x.setter
def device_x(self, motor_name: str) -> None:
motor_name = motor_name or ""
if motor_name == (self.config.x_motor.name or ""):
if motor_name == (self.config.device_x.device or ""):
return
if motor_name and self.y_motor:
self.map(motor_name, self.y_motor, suppress_errors=True)
if motor_name and self.device_y:
self.map(motor_name, self.device_y, suppress_errors=True)
return
self._set_motor_name(axis="x", motor_name=motor_name)
@SafeProperty(str)
def y_motor(self) -> str:
def device_y(self) -> str:
"""Name of the motor shown on the Y axis."""
return self.config.y_motor.name or ""
return self.config.device_y.device or ""
@y_motor.setter
def y_motor(self, motor_name: str) -> None:
@device_y.setter
def device_y(self, motor_name: str) -> None:
motor_name = motor_name or ""
if motor_name == (self.config.y_motor.name or ""):
if motor_name == (self.config.device_y.device or ""):
return
if motor_name and self.x_motor:
self.map(self.x_motor, motor_name, suppress_errors=True)
if motor_name and self.device_x:
self.map(self.device_x, motor_name, suppress_errors=True)
return
self._set_motor_name(axis="y", motor_name=motor_name)
@@ -452,13 +452,13 @@ class MotorMap(PlotBase):
Update stored motor name for given axis and optionally refresh the toolbar selection.
"""
motor_name = motor_name or ""
motor_config = self.config.x_motor if axis == "x" else self.config.y_motor
motor_config = self.config.device_x if axis == "x" else self.config.device_y
if motor_config.name == motor_name:
if motor_config.device == motor_name:
return
motor_config.name = motor_name
self.property_changed.emit(f"{axis}_motor", motor_name)
motor_config.device = motor_name
self.property_changed.emit(f"device_{axis}", motor_name)
if sync_toolbar:
self._sync_motor_map_selection_toolbar()
@@ -468,14 +468,14 @@ class MotorMap(PlotBase):
################################################################################
@SafeSlot()
def map(
self, x_name: str, y_name: str, validate_bec: bool = True, suppress_errors=False
self, device_x: str, device_y: str, validate_bec: bool = True, suppress_errors=False
) -> None:
"""
Set the x and y motor names.
Args:
x_name(str): The name of the x motor.
y_name(str): The name of the y motor.
device_x(str): The name of the x motor.
device_y(str): The name of the y motor.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
suppress_errors(bool, optional): If True, suppress errors during validation. Defaults to False. Used for properties setting. If the validation fails, the changes are not applied.
"""
@@ -484,22 +484,22 @@ class MotorMap(PlotBase):
if validate_bec:
if suppress_errors:
try:
self.entry_validator.validate_signal(x_name, None)
self.entry_validator.validate_signal(y_name, None)
self.entry_validator.validate_signal(device_x, None)
self.entry_validator.validate_signal(device_y, None)
except Exception:
return
else:
self.entry_validator.validate_signal(x_name, None)
self.entry_validator.validate_signal(y_name, None)
self.entry_validator.validate_signal(device_x, None)
self.entry_validator.validate_signal(device_y, None)
self._set_motor_name(axis="x", motor_name=x_name, sync_toolbar=False)
self._set_motor_name(axis="y", motor_name=y_name, sync_toolbar=False)
self._set_motor_name(axis="x", motor_name=device_x, sync_toolbar=False)
self._set_motor_name(axis="y", motor_name=device_y, sync_toolbar=False)
motor_x_limit = self._get_motor_limit(self.config.x_motor.name)
motor_y_limit = self._get_motor_limit(self.config.y_motor.name)
motor_x_limit = self._get_motor_limit(self.config.device_x.device)
motor_y_limit = self._get_motor_limit(self.config.device_y.device)
self.config.x_motor.limits = motor_x_limit
self.config.y_motor.limits = motor_y_limit
self.config.device_x.limits = motor_x_limit
self.config.device_y.limits = motor_y_limit
# reconnect the signals
self._connect_motor_to_slots()
@@ -574,19 +574,19 @@ class MotorMap(PlotBase):
msg(dict): Message from the device readback.
metadata(dict): Metadata of the message.
"""
x_motor = self.config.x_motor.name
y_motor = self.config.y_motor.name
device_x = self.config.device_x.device
device_y = self.config.device_y.device
if x_motor is None or y_motor is None:
if device_x is None or device_y is None:
return
if x_motor in msg["signals"]:
x = msg["signals"][x_motor]["value"]
if device_x in msg["signals"]:
x = msg["signals"][device_x]["value"]
self._buffer["x"].append(x)
self._buffer["y"].append(self._buffer["y"][-1])
elif y_motor in msg["signals"]:
y = msg["signals"][y_motor]["value"]
elif device_y in msg["signals"]:
y = msg["signals"][device_y]["value"]
self._buffer["y"].append(y)
self._buffer["x"].append(self._buffer["x"][-1])
@@ -597,12 +597,12 @@ class MotorMap(PlotBase):
self._disconnect_current_motors()
endpoints_readback = [
MessageEndpoints.device_readback(self.config.x_motor.name),
MessageEndpoints.device_readback(self.config.y_motor.name),
MessageEndpoints.device_readback(self.config.device_x.device),
MessageEndpoints.device_readback(self.config.device_y.device),
]
endpoints_limits = [
MessageEndpoints.device_limits(self.config.x_motor.name),
MessageEndpoints.device_limits(self.config.y_motor.name),
MessageEndpoints.device_limits(self.config.device_x.device),
MessageEndpoints.device_limits(self.config.device_y.device),
]
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints_readback)
@@ -610,14 +610,14 @@ class MotorMap(PlotBase):
def _disconnect_current_motors(self):
"""Disconnect the current motors from the slots."""
if self.config.x_motor.name is not None and self.config.y_motor.name is not None:
if self.config.device_x.device is not None and self.config.device_y.device is not None:
endpoints_readback = [
MessageEndpoints.device_readback(self.config.x_motor.name),
MessageEndpoints.device_readback(self.config.y_motor.name),
MessageEndpoints.device_readback(self.config.device_x.device),
MessageEndpoints.device_readback(self.config.device_y.device),
]
endpoints_limits = [
MessageEndpoints.device_limits(self.config.x_motor.name),
MessageEndpoints.device_limits(self.config.y_motor.name),
MessageEndpoints.device_limits(self.config.device_x.device),
MessageEndpoints.device_limits(self.config.device_y.device),
]
self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints_readback)
self.bec_dispatcher.disconnect_slot(self.on_device_limits, endpoints_limits)
@@ -634,8 +634,8 @@ class MotorMap(PlotBase):
msg(dict): Message from the device limits.
metadata(dict): Metadata of the message.
"""
self.config.x_motor.limits = self._get_motor_limit(self.config.x_motor.name)
self.config.y_motor.limits = self._get_motor_limit(self.config.y_motor.name)
self.config.device_x.limits = self._get_motor_limit(self.config.device_x.device)
self.config.device_y.limits = self._get_motor_limit(self.config.device_y.device)
self._swap_limit_map()
def _get_motor_limit(self, motor: str) -> list | None:
@@ -663,8 +663,8 @@ class MotorMap(PlotBase):
Make the motor map.
"""
motor_x_limit = self.config.x_motor.limits
motor_y_limit = self.config.y_motor.limits
motor_x_limit = self.config.device_x.limits
motor_y_limit = self.config.device_y.limits
self._limit_map = self._make_limit_map(motor_x_limit, motor_y_limit)
self.plot_item.addItem(self._limit_map)
@@ -678,10 +678,10 @@ class MotorMap(PlotBase):
# Add the crosshair for initial motor coordinates
initial_position_x = self._get_motor_init_position(
self.config.x_motor.name, self.config.precision
self.config.device_x.device, self.config.precision
)
initial_position_y = self._get_motor_init_position(
self.config.y_motor.name, self.config.precision
self.config.device_y.device, self.config.precision
)
self._buffer["x"] = [initial_position_x]
@@ -693,8 +693,8 @@ class MotorMap(PlotBase):
self._add_coordinates_crosshair(initial_position_x, initial_position_y)
# Set default labels for the plot
self.set_x_label_suffix(f"[{self.config.x_motor.name}-{self.config.x_motor.name}]")
self.set_y_label_suffix(f"[{self.config.y_motor.name}-{self.config.y_motor.name}]")
self.set_x_label_suffix(f"[{self.config.device_x.device}-{self.config.device_x.device}]")
self.set_y_label_suffix(f"[{self.config.device_y.device}-{self.config.device_y.device}]")
self.update_signal.emit()
@@ -794,8 +794,8 @@ class MotorMap(PlotBase):
def _swap_limit_map(self):
"""Swap the limit map."""
self.plot_item.removeItem(self._limit_map)
x_limits = self.config.x_motor.limits
y_limits = self.config.y_motor.limits
x_limits = self.config.device_x.limits
y_limits = self.config.device_y.limits
if x_limits is not None and y_limits is not None:
self._limit_map = self._make_limit_map(x_limits, y_limits)
self._limit_map.setZValue(-1)
@@ -828,8 +828,8 @@ class MotorMap(PlotBase):
if motor_selection_action is None:
return
motor_selection: MotorSelection = motor_selection_action.widget
target_x = self.config.x_motor.name or ""
target_y = self.config.y_motor.name or ""
target_x = self.config.device_x.device or ""
target_y = self.config.device_y.device or ""
if (
motor_selection.motor_x.currentText() == target_x
@@ -864,10 +864,10 @@ class DemoApp(QMainWindow): # pragma: no cover
self.setCentralWidget(self.main_widget)
self.motor_map_popup = MotorMap(popups=True)
self.motor_map_popup.map(x_name="samx", y_name="samy", validate_bec=True)
self.motor_map_popup.map(device_x="samx", device_y="samy", validate_bec=True)
self.motor_map_side = MotorMap(popups=False)
self.motor_map_side.map(x_name="samx", y_name="samy", validate_bec=True)
self.motor_map_side.map(device_x="samx", device_y="samy", validate_bec=True)
self.layout.addWidget(self.motor_map_side)
self.layout.addWidget(self.motor_map_popup)

View File

@@ -20,8 +20,8 @@ logger = bec_logger.logger
class ScatterDeviceSignal(BaseModel):
"""The configuration of a signal in the scatter waveform widget."""
name: str
entry: str
device: str
signal: str
model_config: dict = {"validate_assignment": True}
@@ -40,13 +40,13 @@ class ScatterCurveConfig(ConnectionConfig):
color_map: str | None = Field(
"plasma", description="The color palette of the figure widget.", validate_default=True
)
x_device: ScatterDeviceSignal | None = Field(
device_x: ScatterDeviceSignal | None = Field(
None, description="The x device signal of the scatter waveform."
)
y_device: ScatterDeviceSignal | None = Field(
device_y: ScatterDeviceSignal | None = Field(
None, description="The y device signal of the scatter waveform."
)
z_device: ScatterDeviceSignal | None = Field(
device_z: ScatterDeviceSignal | None = Field(
None, description="The z device signal of the scatter waveform."
)

View File

@@ -49,18 +49,18 @@ class ScatterWaveform(PlotBase):
"update_with_scan_history",
"clear_all",
# Device properties
"x_device_name",
"x_device_name.setter",
"x_device_entry",
"x_device_entry.setter",
"y_device_name",
"y_device_name.setter",
"y_device_entry",
"y_device_entry.setter",
"z_device_name",
"z_device_name.setter",
"z_device_entry",
"z_device_entry.setter",
"device_x",
"device_x.setter",
"signal_x",
"signal_x.setter",
"device_y",
"device_y.setter",
"signal_y",
"signal_y.setter",
"device_z",
"device_z.setter",
"signal_z",
"signal_z.setter",
]
sync_signal_update = Signal()
@@ -208,12 +208,12 @@ class ScatterWaveform(PlotBase):
@SafeSlot(popup_error=True)
def plot(
self,
x_name: str,
y_name: str,
z_name: str,
x_entry: None | str = None,
y_entry: None | str = None,
z_entry: None | str = None,
device_x: str,
device_y: str,
device_z: str,
signal_x: None | str = None,
signal_y: None | str = None,
signal_z: None | str = None,
color_map: str | None = "plasma",
label: str | None = None,
validate_bec: bool = True,
@@ -222,12 +222,12 @@ class ScatterWaveform(PlotBase):
Plot the data from the device signals.
Args:
x_name (str): The name of the x device signal.
y_name (str): The name of the y device signal.
z_name (str): The name of the z device signal.
x_entry (None | str): The x entry of the device signal.
y_entry (None | str): The y entry of the device signal.
z_entry (None | str): The z entry of the device signal.
device_x (str): The name of the x device signal.
device_y (str): The name of the y device signal.
device_z (str): The name of the z device signal.
signal_x (None | str): The x entry of the device signal.
signal_y (None | str): The y entry of the device signal.
signal_z (None | str): The z entry of the device signal.
color_map (str | None): The color map of the scatter waveform.
label (str | None): The label of the curve.
validate_bec (bool): Whether to validate the device signals with current BEC instance.
@@ -237,9 +237,9 @@ class ScatterWaveform(PlotBase):
"""
if validate_bec:
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
signal_x = self.entry_validator.validate_signal(device_x, signal_x)
signal_y = self.entry_validator.validate_signal(device_y, signal_y)
signal_z = self.entry_validator.validate_signal(device_z, signal_z)
if color_map is not None:
try:
@@ -250,15 +250,15 @@ class ScatterWaveform(PlotBase):
)
if label is None:
label = f"{z_name}-{z_entry}"
label = f"{device_z}-{signal_z}"
config = ScatterCurveConfig(
parent_id=self.gui_id,
label=label,
color_map=color_map,
x_device=ScatterDeviceSignal(name=x_name, entry=x_entry),
y_device=ScatterDeviceSignal(name=y_name, entry=y_entry),
z_device=ScatterDeviceSignal(name=z_name, entry=z_entry),
device_x=ScatterDeviceSignal(device=device_x, signal=signal_x),
device_y=ScatterDeviceSignal(device=device_y, signal=signal_y),
device_z=ScatterDeviceSignal(device=device_z, signal=signal_z),
)
# Add Curve
@@ -350,23 +350,23 @@ class ScatterWaveform(PlotBase):
return "none"
try:
x_name = self._main_curve.config.x_device.name
x_entry = self._main_curve.config.x_device.entry
y_name = self._main_curve.config.y_device.name
y_entry = self._main_curve.config.y_device.entry
z_name = self._main_curve.config.z_device.name
z_entry = self._main_curve.config.z_device.entry
device_x = self._main_curve.config.device_x.device
signal_x = self._main_curve.config.device_x.signal
device_y = self._main_curve.config.device_y.device
signal_y = self._main_curve.config.device_y.signal
device_z = self._main_curve.config.device_z.device
signal_z = self._main_curve.config.device_z.signal
except AttributeError:
return
if access_key == "val":
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
y_data = data.get(y_name, {}).get(y_entry, {}).get(access_key, None)
z_data = data.get(z_name, {}).get(z_entry, {}).get(access_key, None)
x_data = data.get(device_x, {}).get(signal_x, {}).get(access_key, None)
y_data = data.get(device_y, {}).get(signal_y, {}).get(access_key, None)
z_data = data.get(device_z, {}).get(signal_z, {}).get(access_key, None)
else:
x_data = data.get(x_name, {}).get(x_entry, {}).read().get("value", None)
y_data = data.get(y_name, {}).get(y_entry, {}).read().get("value", None)
z_data = data.get(z_name, {}).get(z_entry, {}).read().get("value", None)
x_data = data.get(device_x, {}).get(signal_x, {}).read().get("value", None)
y_data = data.get(device_y, {}).get(signal_y, {}).read().get("value", None)
z_data = data.get(device_z, {}).get(signal_z, {}).read().get("value", None)
self._main_curve.set_data(x=x_data, y=y_data, z=z_data)
@@ -399,14 +399,14 @@ class ScatterWaveform(PlotBase):
################################################################################
@SafeProperty(str)
def x_device_name(self) -> str:
def device_x(self) -> str:
"""Device name for the X axis."""
if self._main_curve is None or self._main_curve.config.x_device is None:
if self._main_curve is None or self._main_curve.config.device_x is None:
return ""
return self._main_curve.config.x_device.name or ""
return self._main_curve.config.device_x.device or ""
@x_device_name.setter
def x_device_name(self, device_name: str) -> None:
@device_x.setter
def device_x(self, device_name: str) -> None:
"""
Set the X device name.
@@ -419,33 +419,33 @@ class ScatterWaveform(PlotBase):
try:
entry = self.entry_validator.validate_signal(device_name, None)
# Update or create config
if self._main_curve.config.x_device is None:
self._main_curve.config.x_device = ScatterDeviceSignal(
name=device_name, entry=entry
if self._main_curve.config.device_x is None:
self._main_curve.config.device_x = ScatterDeviceSignal(
device=device_name, signal=entry
)
else:
self._main_curve.config.x_device.name = device_name
self._main_curve.config.x_device.entry = entry
self.property_changed.emit("x_device_name", device_name)
self._main_curve.config.device_x.device = device_name
self._main_curve.config.device_x.signal = entry
self.property_changed.emit("device_x", device_name)
self.update_labels()
self._try_auto_plot()
except Exception:
pass # Silently fail if device is not available yet
else:
if self._main_curve.config.x_device is not None:
self._main_curve.config.x_device = None
self.property_changed.emit("x_device_name", "")
if self._main_curve.config.device_x is not None:
self._main_curve.config.device_x = None
self.property_changed.emit("device_x", "")
self.update_labels()
@SafeProperty(str)
def x_device_entry(self) -> str:
def signal_x(self) -> str:
"""Signal entry for the X axis device."""
if self._main_curve is None or self._main_curve.config.x_device is None:
if self._main_curve is None or self._main_curve.config.device_x is None:
return ""
return self._main_curve.config.x_device.entry or ""
return self._main_curve.config.device_x.signal or ""
@x_device_entry.setter
def x_device_entry(self, entry: str) -> None:
@signal_x.setter
def signal_x(self, entry: str) -> None:
"""
Set the X device entry.
@@ -455,29 +455,29 @@ class ScatterWaveform(PlotBase):
if not entry:
return
if self._main_curve.config.x_device is None:
logger.warning("Cannot set x_device_entry without x_device_name set first.")
if self._main_curve.config.device_x is None:
logger.warning("Cannot set signal_x without device_x set first.")
return
device_name = self._main_curve.config.x_device.name
device_name = self._main_curve.config.device_x.device
try:
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.x_device.entry = validated_entry
self.property_changed.emit("x_device_entry", validated_entry)
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.device_x.signal = validated_signal
self.property_changed.emit("signal_x", validated_signal)
self.update_labels()
self._try_auto_plot()
except Exception:
pass # Silently fail if validation fails
@SafeProperty(str)
def y_device_name(self) -> str:
def device_y(self) -> str:
"""Device name for the Y axis."""
if self._main_curve is None or self._main_curve.config.y_device is None:
if self._main_curve is None or self._main_curve.config.device_y is None:
return ""
return self._main_curve.config.y_device.name or ""
return self._main_curve.config.device_y.device or ""
@y_device_name.setter
def y_device_name(self, device_name: str) -> None:
@device_y.setter
def device_y(self, device_name: str) -> None:
"""
Set the Y device name.
@@ -490,33 +490,33 @@ class ScatterWaveform(PlotBase):
try:
entry = self.entry_validator.validate_signal(device_name, None)
# Update or create config
if self._main_curve.config.y_device is None:
self._main_curve.config.y_device = ScatterDeviceSignal(
name=device_name, entry=entry
if self._main_curve.config.device_y is None:
self._main_curve.config.device_y = ScatterDeviceSignal(
device=device_name, signal=entry
)
else:
self._main_curve.config.y_device.name = device_name
self._main_curve.config.y_device.entry = entry
self.property_changed.emit("y_device_name", device_name)
self._main_curve.config.device_y.device = device_name
self._main_curve.config.device_y.signal = entry
self.property_changed.emit("device_y", device_name)
self.update_labels()
self._try_auto_plot()
except Exception:
pass # Silently fail if device is not available yet
else:
if self._main_curve.config.y_device is not None:
self._main_curve.config.y_device = None
self.property_changed.emit("y_device_name", "")
if self._main_curve.config.device_y is not None:
self._main_curve.config.device_y = None
self.property_changed.emit("device_y", "")
self.update_labels()
@SafeProperty(str)
def y_device_entry(self) -> str:
def signal_y(self) -> str:
"""Signal entry for the Y axis device."""
if self._main_curve is None or self._main_curve.config.y_device is None:
if self._main_curve is None or self._main_curve.config.device_y is None:
return ""
return self._main_curve.config.y_device.entry or ""
return self._main_curve.config.device_y.signal or ""
@y_device_entry.setter
def y_device_entry(self, entry: str) -> None:
@signal_y.setter
def signal_y(self, entry: str) -> None:
"""
Set the Y device entry.
@@ -526,29 +526,29 @@ class ScatterWaveform(PlotBase):
if not entry:
return
if self._main_curve.config.y_device is None:
logger.warning("Cannot set y_device_entry without y_device_name set first.")
if self._main_curve.config.device_y is None:
logger.warning("Cannot set signal_y without device_y set first.")
return
device_name = self._main_curve.config.y_device.name
device_name = self._main_curve.config.device_y.device
try:
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.y_device.entry = validated_entry
self.property_changed.emit("y_device_entry", validated_entry)
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.device_y.signal = validated_signal
self.property_changed.emit("signal_y", validated_signal)
self.update_labels()
self._try_auto_plot()
except Exception:
pass # Silently fail if validation fails
@SafeProperty(str)
def z_device_name(self) -> str:
def device_z(self) -> str:
"""Device name for the Z (color) axis."""
if self._main_curve is None or self._main_curve.config.z_device is None:
if self._main_curve is None or self._main_curve.config.device_z is None:
return ""
return self._main_curve.config.z_device.name or ""
return self._main_curve.config.device_z.device or ""
@z_device_name.setter
def z_device_name(self, device_name: str) -> None:
@device_z.setter
def device_z(self, device_name: str) -> None:
"""
Set the Z device name.
@@ -561,33 +561,33 @@ class ScatterWaveform(PlotBase):
try:
entry = self.entry_validator.validate_signal(device_name, None)
# Update or create config
if self._main_curve.config.z_device is None:
self._main_curve.config.z_device = ScatterDeviceSignal(
name=device_name, entry=entry
if self._main_curve.config.device_z is None:
self._main_curve.config.device_z = ScatterDeviceSignal(
device=device_name, signal=entry
)
else:
self._main_curve.config.z_device.name = device_name
self._main_curve.config.z_device.entry = entry
self.property_changed.emit("z_device_name", device_name)
self._main_curve.config.device_z.device = device_name
self._main_curve.config.device_z.signal = entry
self.property_changed.emit("device_z", device_name)
self.update_labels()
self._try_auto_plot()
except Exception:
pass # Silently fail if device is not available yet
else:
if self._main_curve.config.z_device is not None:
self._main_curve.config.z_device = None
self.property_changed.emit("z_device_name", "")
if self._main_curve.config.device_z is not None:
self._main_curve.config.device_z = None
self.property_changed.emit("device_z", "")
self.update_labels()
@SafeProperty(str)
def z_device_entry(self) -> str:
def signal_z(self) -> str:
"""Signal entry for the Z (color) axis device."""
if self._main_curve is None or self._main_curve.config.z_device is None:
if self._main_curve is None or self._main_curve.config.device_z is None:
return ""
return self._main_curve.config.z_device.entry or ""
return self._main_curve.config.device_z.signal or ""
@z_device_entry.setter
def z_device_entry(self, entry: str) -> None:
@signal_z.setter
def signal_z(self, entry: str) -> None:
"""
Set the Z device entry.
@@ -597,15 +597,15 @@ class ScatterWaveform(PlotBase):
if not entry:
return
if self._main_curve.config.z_device is None:
logger.warning("Cannot set z_device_entry without z_device_name set first.")
if self._main_curve.config.device_z is None:
logger.warning("Cannot set signal_z without device_z set first.")
return
device_name = self._main_curve.config.z_device.name
device_name = self._main_curve.config.device_z.device
try:
validated_entry = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.z_device.entry = validated_entry
self.property_changed.emit("z_device_entry", validated_entry)
validated_signal = self.entry_validator.validate_signal(device_name, entry)
self._main_curve.config.device_z.signal = validated_signal
self.property_changed.emit("signal_z", validated_signal)
self.update_labels()
self._try_auto_plot()
except Exception:
@@ -615,25 +615,25 @@ class ScatterWaveform(PlotBase):
"""
Attempt to automatically call plot() if all three devices are set.
"""
has_x = self._main_curve.config.x_device is not None
has_y = self._main_curve.config.y_device is not None
has_z = self._main_curve.config.z_device is not None
has_x = self._main_curve.config.device_x is not None
has_y = self._main_curve.config.device_y is not None
has_z = self._main_curve.config.device_z is not None
if has_x and has_y and has_z:
x_name = self._main_curve.config.x_device.name
x_entry = self._main_curve.config.x_device.entry
y_name = self._main_curve.config.y_device.name
y_entry = self._main_curve.config.y_device.entry
z_name = self._main_curve.config.z_device.name
z_entry = self._main_curve.config.z_device.entry
device_x = self._main_curve.config.device_x.device
signal_x = self._main_curve.config.device_x.signal
device_y = self._main_curve.config.device_y.device
signal_y = self._main_curve.config.device_y.signal
device_z = self._main_curve.config.device_z.device
signal_z = self._main_curve.config.device_z.signal
try:
self.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
device_x=device_x,
device_y=device_y,
device_z=device_z,
signal_x=signal_x,
signal_y=signal_y,
signal_z=signal_z,
validate_bec=False, # Don't validate - entries already validated
)
except Exception as e:
@@ -650,21 +650,21 @@ class ScatterWaveform(PlotBase):
config = self._main_curve.config
# Safely get device names
x_device = config.x_device
y_device = config.y_device
device_x = config.device_x
device_y = config.device_y
x_name = x_device.name if x_device else None
y_name = y_device.name if y_device else None
device_x = device_x.device if device_x else None
device_y = device_y.device if device_y else None
if x_name is not None:
self.x_label = x_name # type: ignore
x_dev = self.dev.get(x_name)
if device_x is not None:
self.x_label = device_x # type: ignore
x_dev = self.dev.get(device_x)
if x_dev and hasattr(x_dev, "egu"):
self.x_label_units = x_dev.egu()
if y_name is not None:
self.y_label = y_name # type: ignore
y_dev = self.dev.get(y_name)
if device_y is not None:
self.y_label = device_y # type: ignore
y_dev = self.dev.get(device_y)
if y_dev and hasattr(y_dev, "egu"):
self.y_label_units = y_dev.egu()
@@ -756,7 +756,7 @@ class DemoApp(QMainWindow): # pragma: no cover
self.setCentralWidget(self.main_widget)
self.waveform_popup = ScatterWaveform(popups=True)
self.waveform_popup.plot("samx", "samy", "bpm4i")
self.waveform_popup.plot(device_x="samx", device_y="samy", device_z="bpm4i")
self.waveform_side = ScatterWaveform(popups=False)
self.waveform_popup.plot("samx", "samy", "bpm3a")

View File

@@ -58,81 +58,81 @@ class ScatterCurveSettings(SettingWidget):
color_map = getattr(self.target_widget, "color_map", None)
# Default values for device properties
x_name, x_entry = None, None
y_name, y_entry = None, None
z_name, z_entry = None, None
device_x, signal_x = None, None
device_y, signal_y = None, None
device_z, signal_z = None, None
# Safely access device properties
if hasattr(self.target_widget, "main_curve") and self.target_widget.main_curve:
if hasattr(self.target_widget.main_curve, "config"):
config = self.target_widget.main_curve.config
if hasattr(config, "x_device") and config.x_device:
x_name = getattr(config.x_device, "name", None)
x_entry = getattr(config.x_device, "entry", None)
if hasattr(config, "device_x") and config.device_x:
device_x = getattr(config.device_x, "device", None)
signal_x = getattr(config.device_x, "signal", None)
if hasattr(config, "y_device") and config.y_device:
y_name = getattr(config.y_device, "name", None)
y_entry = getattr(config.y_device, "entry", None)
if hasattr(config, "device_y") and config.device_y:
device_y = getattr(config.device_y, "device", None)
signal_y = getattr(config.device_y, "signal", None)
if hasattr(config, "z_device") and config.z_device:
z_name = getattr(config.z_device, "name", None)
z_entry = getattr(config.z_device, "entry", None)
if hasattr(config, "device_z") and config.device_z:
device_z = getattr(config.device_z, "device", None)
signal_z = getattr(config.device_z, "signal", None)
# Apply the properties to the settings widget
if hasattr(self.ui, "color_map"):
self.ui.color_map.colormap = color_map
if hasattr(self.ui, "x_name"):
self.ui.x_name.set_device(x_name)
if hasattr(self.ui, "x_entry") and x_entry is not None:
self.ui.x_entry.set_to_obj_name(x_entry)
if hasattr(self.ui, "device_x"):
self.ui.device_x.set_device(device_x)
if hasattr(self.ui, "signal_x") and signal_x is not None:
self.ui.signal_x.set_to_obj_name(signal_x)
if hasattr(self.ui, "y_name"):
self.ui.y_name.set_device(y_name)
if hasattr(self.ui, "y_entry") and y_entry is not None:
self.ui.y_entry.set_to_obj_name(y_entry)
if hasattr(self.ui, "device_y"):
self.ui.device_y.set_device(device_y)
if hasattr(self.ui, "signal_y") and signal_y is not None:
self.ui.signal_y.set_to_obj_name(signal_y)
if hasattr(self.ui, "z_name"):
self.ui.z_name.set_device(z_name)
if hasattr(self.ui, "z_entry") and z_entry is not None:
self.ui.z_entry.set_to_obj_name(z_entry)
if hasattr(self.ui, "device_z"):
self.ui.device_z.set_device(device_z)
if hasattr(self.ui, "signal_z") and signal_z is not None:
self.ui.signal_z.set_to_obj_name(signal_z)
@SafeSlot()
def accept_changes(self):
"""
Apply all properties from the settings widget to the target widget.
"""
x_name = self.ui.x_name.currentText()
x_entry = self.ui.x_entry.get_signal_name()
y_name = self.ui.y_name.currentText()
y_entry = self.ui.y_entry.get_signal_name()
z_name = self.ui.z_name.currentText()
z_entry = self.ui.z_entry.get_signal_name()
device_x = self.ui.device_x.currentText()
signal_x = self.ui.signal_x.get_signal_name()
device_y = self.ui.device_y.currentText()
signal_y = self.ui.signal_y.get_signal_name()
device_z = self.ui.device_z.currentText()
signal_z = self.ui.signal_z.get_signal_name()
validate_bec = self.ui.validate_bec.checked
color_map = self.ui.color_map.colormap
self.target_widget.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
device_x=device_x,
device_y=device_y,
device_z=device_z,
signal_x=signal_x,
signal_y=signal_y,
signal_z=signal_z,
color_map=color_map,
validate_bec=validate_bec,
)
def cleanup(self):
self.ui.x_name.close()
self.ui.x_name.deleteLater()
self.ui.x_entry.close()
self.ui.x_entry.deleteLater()
self.ui.y_name.close()
self.ui.y_name.deleteLater()
self.ui.y_entry.close()
self.ui.y_entry.deleteLater()
self.ui.z_name.close()
self.ui.z_name.deleteLater()
self.ui.z_entry.close()
self.ui.z_entry.deleteLater()
self.ui.device_x.close()
self.ui.device_x.deleteLater()
self.ui.signal_x.close()
self.ui.signal_x.deleteLater()
self.ui.device_y.close()
self.ui.device_y.deleteLater()
self.ui.signal_y.close()
self.ui.signal_y.deleteLater()
self.ui.device_z.close()
self.ui.device_z.deleteLater()
self.ui.signal_z.close()
self.ui.signal_z.deleteLater()

View File

@@ -61,7 +61,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="x_name">
<widget class="DeviceComboBox" name="device_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -71,7 +71,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="x_entry">
<widget class="SignalComboBox" name="signal_x">
<property name="editable">
<bool>true</bool>
</property>
@@ -101,7 +101,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="y_name">
<widget class="DeviceComboBox" name="device_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -111,7 +111,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="y_entry">
<widget class="SignalComboBox" name="signal_y">
<property name="editable">
<bool>true</bool>
</property>
@@ -141,7 +141,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceComboBox" name="z_name">
<widget class="DeviceComboBox" name="device_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -151,7 +151,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="SignalComboBox" name="z_entry">
<widget class="SignalComboBox" name="signal_z">
<property name="editable">
<bool>true</bool>
</property>
@@ -187,19 +187,19 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>x_name</tabstop>
<tabstop>y_name</tabstop>
<tabstop>z_name</tabstop>
<tabstop>x_entry</tabstop>
<tabstop>y_entry</tabstop>
<tabstop>z_entry</tabstop>
<tabstop>device_x</tabstop>
<tabstop>device_y</tabstop>
<tabstop>device_z</tabstop>
<tabstop>signal_x</tabstop>
<tabstop>signal_y</tabstop>
<tabstop>signal_z</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>device_reset()</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -213,9 +213,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>device_reset()</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -229,9 +229,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>device_reset()</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>reset_selection()</slot>
<hints>
<hint type="sourcelabel">
@@ -245,9 +245,9 @@
</hints>
</connection>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -261,9 +261,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">
@@ -277,9 +277,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>set_device(QString)</slot>
<hints>
<hint type="sourcelabel">

View File

@@ -58,7 +58,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceLineEdit" name="x_name"/>
<widget class="DeviceLineEdit" name="device_x"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
@@ -68,7 +68,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="x_entry"/>
<widget class="QLineEdit" name="signal_x"/>
</item>
</layout>
</widget>
@@ -87,7 +87,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceLineEdit" name="y_name"/>
<widget class="DeviceLineEdit" name="device_y"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
@@ -97,7 +97,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="y_entry"/>
<widget class="QLineEdit" name="signal_y"/>
</item>
</layout>
</widget>
@@ -116,7 +116,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="DeviceLineEdit" name="z_name"/>
<widget class="DeviceLineEdit" name="device_z"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
@@ -126,7 +126,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="z_entry"/>
<widget class="QLineEdit" name="signal_z"/>
</item>
</layout>
</widget>
@@ -153,9 +153,9 @@
<resources/>
<connections>
<connection>
<sender>x_name</sender>
<sender>device_x</sender>
<signal>textChanged(QString)</signal>
<receiver>x_entry</receiver>
<receiver>signal_x</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">
@@ -169,9 +169,9 @@
</hints>
</connection>
<connection>
<sender>y_name</sender>
<sender>device_y</sender>
<signal>textChanged(QString)</signal>
<receiver>y_entry</receiver>
<receiver>signal_y</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">
@@ -185,9 +185,9 @@
</hints>
</connection>
<connection>
<sender>z_name</sender>
<sender>device_z</sender>
<signal>textChanged(QString)</signal>
<receiver>z_entry</receiver>
<receiver>signal_z</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">

View File

@@ -20,8 +20,8 @@ logger = bec_logger.logger
class DeviceSignal(BaseModel):
"""The configuration of a signal in the 1D waveform widget."""
name: str
entry: str
device: str
signal: str
dap: str | None = None
dap_oversample: int = 1

View File

@@ -140,7 +140,7 @@ class CurveSetting(SettingWidget):
signal_x = self.signal_x.currentText()
signal_data = self.signal_x.itemData(self.signal_x.currentIndex())
if signal_x != "":
self.target_widget.x_entry = signal_data.get("obj_name", signal_x)
self.target_widget.signal_x = signal_data.get("obj_name", signal_x)
else:
self.target_widget.x_mode = self.mode_combo.currentText()
self.curve_manager.send_curve_json()

View File

@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_qthemes._icon.material_icons import material_icon
from qtpy.QtGui import QValidator
from qtpy.QtWidgets import QApplication
class ScanIndexValidator(QValidator):
@@ -226,7 +225,7 @@ class CurveRow(QTreeWidgetItem):
self.device_edit.currentTextChanged.connect(self.entry_edit.set_device)
self.device_edit.device_reset.connect(self.entry_edit.reset_selection)
if self.config.signal:
device_index = self.device_edit.findText(self.config.signal.name or "")
device_index = self.device_edit.findText(self.config.signal.device or "")
if device_index >= 0:
self.device_edit.setCurrentIndex(device_index)
# Force the entry_edit to update based on the device name
@@ -235,7 +234,7 @@ class CurveRow(QTreeWidgetItem):
# If the device name is not found, set the first enabled item
self.device_edit.setCurrentIndex(0)
if not self.entry_edit.set_to_obj_name(self.config.signal.entry):
if not self.entry_edit.set_to_obj_name(self.config.signal.signal):
# If the entry is not found, try to set it to the first enabled item
if not self.entry_edit.set_to_first_enabled():
# If no enabled item is found, set to the first item
@@ -309,15 +308,15 @@ class CurveRow(QTreeWidgetItem):
dev_name = ""
dev_entry = ""
if self.config.signal:
dev_name = self.config.signal.name
dev_entry = self.config.signal.entry
dev_name = self.config.signal.device
dev_entry = self.config.signal.signal
# Create a new config for the DAP row
dap_cfg = CurveConfig(
widget_class="Curve",
source="dap",
parent_label=parent_label,
signal=DeviceSignal(name=dev_name, entry=dev_entry),
signal=DeviceSignal(device=dev_name, signal=dev_entry),
)
new_dap = CurveRow(self.tree, parent_item=self, config=dap_cfg, device_manager=self.dev)
# Expand device row to show new child
@@ -395,10 +394,10 @@ class CurveRow(QTreeWidgetItem):
device_entry = device_entry_info.get("obj_name", device_entry)
else:
device_entry = self.entry_validator.validate_signal(
name=device_name, entry=device_entry
device=device_name, signal=device_entry
)
self.config.signal = DeviceSignal(name=device_name, entry=device_entry)
self.config.signal = DeviceSignal(device=device_name, signal=device_entry)
scan_combo_text = self.scan_index_combo.currentText()
if scan_combo_text == "live" or scan_combo_text == "":
self.config.scan_number = None
@@ -422,16 +421,16 @@ class CurveRow(QTreeWidgetItem):
if self.parent_item:
parent_conf_dict = self.parent_item.export_data()
parent_conf = CurveConfig(**parent_conf_dict)
dev_name = ""
dev_entry = ""
device = ""
signal = ""
if parent_conf.signal:
dev_name = parent_conf.signal.name
dev_entry = parent_conf.signal.entry
device = parent_conf.signal.device
signal = parent_conf.signal.signal
# Dap from the DapComboBox
new_dap = "GaussianModel"
if hasattr(self, "dap_combo"):
new_dap = self.dap_combo.fit_model_combobox.currentText()
self.config.signal = DeviceSignal(name=dev_name, entry=dev_entry, dap=new_dap)
self.config.signal = DeviceSignal(device=device, signal=signal, dap=new_dap)
self.config.source = "dap"
self.config.parent_label = parent_conf.label
self.config.label = f"{parent_conf.label}-{new_dap}"
@@ -613,15 +612,12 @@ class CurveTree(BECWidget, QWidget):
item.config.color = new_col
item.config.symbol_color = new_col
def add_new_curve(self, name: str = None, entry: str = None):
def add_new_curve(self, device: str = None, signal: str = None):
"""Add a new device-type CurveRow with an assigned colormap color.
Args:
name (str, optional): Device name.
entry (str, optional): Device entry.
style (str, optional): Pen style. Defaults to "solid".
width (int, optional): Pen width. Defaults to 4.
symbol_size (int, optional): Symbol size. Defaults to 7.
device (str, optional): Device name.
signal (str, optional): Device entry.
Returns:
CurveRow: The newly created top-level row.
@@ -630,7 +626,7 @@ class CurveTree(BECWidget, QWidget):
widget_class="Curve",
parent_id=self.waveform.gui_id,
source="device",
signal=DeviceSignal(name=name or "", entry=entry or ""),
signal=DeviceSignal(device=device or "", signal=signal or ""),
)
new_row = CurveRow(self.tree, parent_item=None, config=cfg, device_manager=self.dev)

View File

@@ -73,8 +73,8 @@ class Waveform(PlotBase):
"curves",
"x_mode",
"x_mode.setter",
"x_entry",
"x_entry.setter",
"signal_x",
"signal_x.setter",
"color_palette",
"color_palette.setter",
"skip_large_dataset_warning",
@@ -409,7 +409,7 @@ class Waveform(PlotBase):
self.scan_history_dialog.layout.addWidget(self.scan_history_widget)
self.scan_history_widget.scan_history_device_viewer.request_history_plot.connect(
lambda scan_id, device_name, signal_name: self.plot(
y_name=device_name, y_entry=signal_name, scan_id=scan_id
device_y=device_name, signal_y=signal_name, scan_id=scan_id
)
)
self.scan_history_dialog.finished.connect(self._scan_history_closed)
@@ -534,14 +534,14 @@ class Waveform(PlotBase):
self.round_plot_widget.apply_plot_widget_style() # To keep the correct theme
@SafeProperty(str)
def x_entry(self) -> str | None:
def signal_x(self) -> str | None:
"""
The x signal name.
"""
return self.x_axis_mode["entry"]
@x_entry.setter
def x_entry(self, value: str | None):
@signal_x.setter
def signal_x(self, value: str | None):
"""
Set the x signal name.
@@ -551,7 +551,7 @@ class Waveform(PlotBase):
if value is None:
return
if self.x_axis_mode["name"] in ["auto", "index", "timestamp"]:
logger.warning("Cannot set x_entry when x_mode is not 'device'.")
logger.warning("Cannot set signal_x when x_mode is not 'device'.")
return
self.x_axis_mode["entry"] = self.entry_validator.validate_signal(self.x_mode, value)
self._switch_x_axis_item(mode="device")
@@ -690,10 +690,10 @@ class Waveform(PlotBase):
arg1: list | np.ndarray | str | None = None,
y: list | np.ndarray | None = None,
x: list | np.ndarray | None = None,
x_name: str | None = None,
y_name: str | None = None,
x_entry: str | None = None,
y_entry: str | None = None,
device_x: str | None = None,
device_y: str | None = None,
signal_x: str | None = None,
signal_y: str | None = None,
color: str | None = None,
label: str | None = None,
dap: str | None = None,
@@ -705,17 +705,17 @@ class Waveform(PlotBase):
Plot a curve to the plot widget.
Args:
arg1(list | np.ndarray | str | None): First argument, which can be x data, y data, or y_name.
arg1(list | np.ndarray | str | None): First argument, which can be x data, y data, or device_y.
y(list | np.ndarray): Custom y data to plot.
x(list | np.ndarray): Custom y data to plot.
x_name(str): Name of the x signal.
device_x(str): Name of the x signal.
- "auto": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of a device from BEC.
y_name(str): The name of the device for the y-axis.
x_entry(str): The name of the entry for the x-axis.
y_entry(str): The name of the entry for the y-axis.
device_y(str): The name of the device for the y-axis.
signal_x(str): The name of the entry for the x-axis.
signal_y(str): The name of the entry for the y-axis.
color(str): The color of the curve.
label(str): The label of the curve.
dap(str): The dap model to use for the curve. When provided, a DAP curve is
@@ -741,7 +741,7 @@ class Waveform(PlotBase):
y_data = np.asarray(y)
if isinstance(arg1, str):
y_name = arg1
device_y = arg1
elif isinstance(arg1, list):
if isinstance(y, list):
source = "custom"
@@ -762,17 +762,17 @@ class Waveform(PlotBase):
x_data = arg1[:, 0]
y_data = arg1[:, 1]
# If y_name is set => device data
if y_name is not None and x_data is None and y_data is None:
# If device_y is set => device data
if device_y is not None and x_data is None and y_data is None:
source = "device"
# Validate or obtain entry
y_entry = self.entry_validator.validate_signal(name=y_name, entry=y_entry)
signal_y = self.entry_validator.validate_signal(device_y, signal_y)
# If user gave x_name => store in x_axis_mode, but do not set data here
if x_name is not None:
self.x_mode = x_name
if x_name not in ["timestamp", "index", "auto"]:
self.x_axis_mode["entry"] = self.entry_validator.validate_signal(x_name, x_entry)
# If user gave device_x => store in x_axis_mode, but do not set data here
if device_x is not None:
self.x_mode = device_x
if device_x not in ["timestamp", "index", "auto"]:
self.x_axis_mode["entry"] = self.entry_validator.validate_signal(device_x, signal_x)
# Decide label if not provided
if label is None:
@@ -781,7 +781,7 @@ class Waveform(PlotBase):
"Curve", [c.object_name for c in self.curves]
)
else:
label = f"{y_name}-{y_entry}"
label = f"{device_y}-{signal_y}"
# If color not provided, generate from palette
if color is None:
@@ -801,7 +801,7 @@ class Waveform(PlotBase):
# If it's device-based, attach DeviceSignal
if source == "device":
config.signal = DeviceSignal(name=y_name, entry=y_entry)
config.signal = DeviceSignal(device=device_y, signal=signal_y)
if scan_id is not None or scan_number is not None:
config.source = "history"
@@ -851,8 +851,8 @@ class Waveform(PlotBase):
f"Only device, history, or custom curves support fitting."
)
dev_name = getattr(getattr(device_curve.config, "signal", None), "name", None)
dev_entry = getattr(getattr(device_curve.config, "signal", None), "entry", None)
dev_name = getattr(getattr(device_curve.config, "signal", None), "device", None)
dev_entry = getattr(getattr(device_curve.config, "signal", None), "signal", None)
if dev_name is None:
dev_name = device_label
if dev_entry is None:
@@ -882,7 +882,7 @@ class Waveform(PlotBase):
# Attach device signal with DAP
config.signal = DeviceSignal(
name=dev_name, entry=dev_entry, dap=dap_name, dap_oversample=dap_oversample
device=dev_name, signal=dev_entry, dap=dap_name, dap_oversample=dap_oversample
)
# 4) Create the DAP curve config using `_add_curve(...)`
@@ -927,7 +927,7 @@ class Waveform(PlotBase):
label = config.label
if config.source == "history":
label = f"{config.signal.name}-{config.signal.entry}-scan-{config.scan_number}"
label = f"{config.signal.device}-{config.signal.signal}-scan-{config.scan_number}"
config.label = label
if not label:
# Fallback label
@@ -1003,8 +1003,8 @@ class Waveform(PlotBase):
self, curve: Curve, scan_item: ScanDataContainer
) -> Curve | None:
# Check if the data are already set
device = curve.config.signal.name
entry = curve.config.signal.entry
device = curve.config.signal.device
entry = curve.config.signal.signal
all_devices_used = getattr(
getattr(scan_item, "_msg", None), "stored_data_info", None
@@ -1043,20 +1043,20 @@ class Waveform(PlotBase):
)
curve.setVisible(False)
return
x_entry_custom = self.x_axis_mode.get("entry")
if x_entry_custom is None:
x_entry_custom = self.entry_validator.validate_signal(
signal_x_custom = self.x_axis_mode.get("entry")
if signal_x_custom is None:
signal_x_custom = self.entry_validator.validate_signal(
self.x_axis_mode["name"], None
)
if x_entry_custom not in all_devices_used[self.x_axis_mode["name"]]:
if signal_x_custom not in all_devices_used[self.x_axis_mode["name"]]:
logger.warning(
f"Custom entry '{x_entry_custom}' for device '{self.x_axis_mode['name']}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
f"Custom entry '{signal_x_custom}' for device '{self.x_axis_mode['name']}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
)
curve.setVisible(False)
return
x_shape = (
scan_item._msg.stored_data_info.get(self.x_axis_mode["name"])
.get(x_entry_custom)
.get(signal_x_custom)
.shape[0]
)
if x_shape != y_shape:
@@ -1066,9 +1066,9 @@ class Waveform(PlotBase):
curve.setVisible(False)
return
x_device = scan_item.devices.get(self.x_axis_mode["name"])
x_data = x_device.get(x_entry_custom).read().get("value")
x_data = x_device.get(signal_x_custom).read().get("value")
curve.config.current_x_mode = self.x_axis_mode["name"]
self._update_x_label_suffix(f" (custom: {self.x_axis_mode['name']}-{x_entry_custom})")
self._update_x_label_suffix(f" (custom: {self.x_axis_mode['name']}-{signal_x_custom})")
elif self.x_axis_mode["name"] == "auto":
if (
self._current_x_device is None
@@ -1083,24 +1083,24 @@ class Waveform(PlotBase):
curve.set_data(x=x_data, y=y_data)
self._update_x_label_suffix(" (auto: index)")
return curve
x_entry = self.entry_validator.validate_signal(scan_motors[0], None)
if x_entry not in all_devices_used.get(scan_motors[0], {}):
signal_x = self.entry_validator.validate_signal(scan_motors[0], None)
if signal_x not in all_devices_used.get(scan_motors[0], {}):
logger.warning(
f"Auto x entry '{x_entry}' for device '{scan_motors[0]}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
f"Auto x entry '{signal_x}' for device '{scan_motors[0]}' not found in scan item of history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
)
curve.setVisible(False)
return
if y_shape != all_devices_used.get(scan_motors[0]).get(x_entry, {}).shape[0]:
if y_shape != all_devices_used.get(scan_motors[0]).get(signal_x, {}).shape[0]:
logger.warning(
f"Shape mismatch for x data '{all_devices_used.get(scan_motors[0]).get(x_entry, {}).get('shape', [0])[0]}' and y data '{y_shape}' in history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
f"Shape mismatch for x data '{all_devices_used.get(scan_motors[0]).get(signal_x, {}).get('shape', [0])[0]}' and y data '{y_shape}' in history curve '{curve.name()}'; scan ID: {curve.config.scan_id}."
)
curve.setVisible(False)
return
x_data = scan_item.devices.get(scan_motors[0]).get(x_entry).read().get("value")
self._current_x_device = (scan_motors[0], x_entry)
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{x_entry})")
x_data = scan_item.devices.get(scan_motors[0]).get(signal_x).read().get("value")
self._current_x_device = (scan_motors[0], signal_x)
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{signal_x})")
curve.config.current_x_mode = "auto"
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{x_entry})")
self._update_x_label_suffix(f" (auto: {scan_motors[0]}-{signal_x})")
else: # Scan in auto mode was done and live scan already set the current x device
if self._current_x_device[0] not in all_devices_used:
logger.warning(
@@ -1446,8 +1446,8 @@ class Waveform(PlotBase):
return
data, access_key = self._fetch_scan_data_and_access()
for curve in self._sync_curves:
device_name = curve.config.signal.name
device_entry = curve.config.signal.entry
device_name = curve.config.signal.device
device_entry = curve.config.signal.signal
if access_key == "val":
device_data = data.get(device_name, {}).get(device_entry, {}).get(access_key, None)
else:
@@ -1481,8 +1481,8 @@ class Waveform(PlotBase):
data, access_key = self._fetch_scan_data_and_access()
for curve in self._async_curves:
device_name = curve.config.signal.name
device_entry = curve.config.signal.entry
device_name = curve.config.signal.device
device_entry = curve.config.signal.signal
if access_key == "val": # live access
device_data = data.get(device_name, {}).get(device_entry, {}).get(access_key, None)
else: # history access
@@ -1535,8 +1535,8 @@ class Waveform(PlotBase):
bec_async_signals = self.client.device_manager.get_bec_signals(
["AsyncSignal", "AsyncMultiSignal"]
)
for entry_name, _, entry_data in bec_async_signals:
if entry_name == name and entry_data.get("obj_name") == signal:
for signal_name, _, entry_data in bec_async_signals:
if signal_name == name and entry_data.get("obj_name") == signal:
return True, entry_data.get("storage_name")
return False, signal
@@ -1547,8 +1547,8 @@ class Waveform(PlotBase):
Args:
curve(Curve): The curve to set up.
"""
name = curve.config.signal.name
signal = curve.config.signal.entry
name = curve.config.signal.device
signal = curve.config.signal.signal
async_signal_found, signal = self._check_async_signal_found(name, signal)
try:
@@ -1621,7 +1621,7 @@ class Waveform(PlotBase):
x_data = None # Reset x_data
y_data = None # Reset y_data
# Get the curve data
async_data = msg["signals"].get(curve.config.signal.entry, None)
async_data = msg["signals"].get(curve.config.signal.signal, None)
if async_data is None:
continue
# y-data
@@ -1665,12 +1665,12 @@ class Waveform(PlotBase):
# x_axis_mode is device signal
# Only consider device signals that are async for now, fallback is index
x_device_entry = self.x_axis_mode["entry"]
async_data = msg["signals"].get(x_device_entry, None)
signal_x = self.x_axis_mode["entry"]
async_data = msg["signals"].get(signal_x, None)
# Make sure the signal exists, otherwise fall back to index
if async_data is None:
# Try to grab the data from device signals
data_plot_x = self._get_x_data(plot_mode, x_device_entry)
data_plot_x = self._get_x_data(plot_mode, signal_x)
else:
data_plot_x = np.asarray(async_data["value"])
if x_data is not None:
@@ -1678,7 +1678,7 @@ class Waveform(PlotBase):
# Fallback incase data is not of equal length
if len(data_plot_x) != len(data_plot_y):
logger.warning(
f"Async data for curve {curve.name()} and x_axis {x_device_entry} is not of equal length. Falling back to 'index' plotting."
f"Async data for curve {curve.name()} and x_axis {signal_x} is not of equal length. Falling back to 'index' plotting."
)
data_plot_x = np.linspace(0, len(data_plot_y) - 1, len(data_plot_y))
@@ -1858,18 +1858,18 @@ class Waveform(PlotBase):
# 1 User wants custom signal
if self.x_axis_mode["name"] not in ["timestamp", "index", "auto"]:
x_name = self.x_axis_mode["name"]
x_entry = self.x_axis_mode.get("entry", None)
if x_entry is None:
x_entry = self.entry_validator.validate_signal(x_name, None)
device_x = self.x_axis_mode["name"]
signal_x = self.x_axis_mode.get("entry", None)
if signal_x is None:
signal_x = self.entry_validator.validate_signal(device_x, None)
# if the motor was not scanned, an empty list is returned and curves are not updated
if access_key == "val": # live data
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, [0])
x_data = data.get(device_x, {}).get(signal_x, {}).get(access_key, [0])
else: # history data
entry_obj = data.get(x_name, {}).get(x_entry)
entry_obj = data.get(device_x, {}).get(signal_x)
x_data = entry_obj.read()["value"] if entry_obj else [0]
new_suffix = f" (custom: {x_name}-{x_entry})"
self._current_x_device = (x_name, x_entry)
new_suffix = f" (custom: {device_x}-{signal_x})"
self._current_x_device = (device_x, signal_x)
# 2 User wants timestamp
if self.x_axis_mode["name"] == "timestamp":
@@ -1913,15 +1913,15 @@ class Waveform(PlotBase):
x_data = None
new_suffix = " (auto: index)"
else:
x_name = scan_report_devices[0]
x_entry = self.entry_validator.validate_signal(x_name, None)
device_x = scan_report_devices[0]
signal_x = self.entry_validator.validate_signal(device_x, None)
if access_key == "val":
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
x_data = data.get(device_x, {}).get(signal_x, {}).get(access_key, None)
else:
entry_obj = data.get(x_name, {}).get(x_entry)
entry_obj = data.get(device_x, {}).get(signal_x)
x_data = entry_obj.read()["value"] if entry_obj else None
new_suffix = f" (auto: {x_name}-{x_entry})"
self._current_x_device = (x_name, x_entry)
new_suffix = f" (auto: {device_x}-{signal_x})"
self._current_x_device = (device_x, signal_x)
self._update_x_label_suffix(new_suffix)
return x_data
@@ -1999,7 +1999,7 @@ class Waveform(PlotBase):
for curve in self.curves:
if curve.config.source != "device":
continue
dev_name = curve.config.signal.name
dev_name = curve.config.signal.device
if dev_name in readout_priority_async:
self._async_curves.append(curve)
if hasattr(self.scan_item, "live_data"):
@@ -2347,11 +2347,11 @@ class DemoApp(QMainWindow): # pragma: no cover
self.setCentralWidget(self.main_widget)
self.waveform_popup = Waveform(popups=True)
self.waveform_popup.plot(y_name="waveform")
self.waveform_popup.plot(device_y="waveform")
self.waveform_side = Waveform(popups=False)
self.waveform_side.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
self.waveform_side.plot(y_name="bpm3a", y_entry="bpm3a")
self.waveform_side.plot(device_y="bpm4i", signal_y="bpm4i", dap="GaussianModel")
self.waveform_side.plot(device_y="bpm3a", signal_y="bpm3a")
self.custom_waveform = Waveform(popups=True)
self._populate_custom_curve_demo()

View File

@@ -55,7 +55,7 @@ bec_figure = BECFigure(gui_id="my_gui_app_id")
window.setCentralWidget(bec_figure)
# prepare to plot samx motor vs bpm4i value
bec_figure.plot(x_name="samx", y_name="bpm4i")
bec_figure.plot(device_x="samx", device_y="bpm4i")
```
In the example just above, the resulting application will show a plot of samx
@@ -96,7 +96,7 @@ window = QMainWindow()
bec_figure = BECFigure(parent=window, gui_id="my_gui_app_id")
window.setCentralWidget(bec_figure)
bec_figure.plot(x_name="samx", y_name="bpm4i")
bec_figure.plot(device_x="samx", device_y="bpm4i")
# ensuring proper cleanup
def final_cleanup():

View File

@@ -45,7 +45,7 @@ For the introduction given here, we will focus on the plotting widgets of BECWid
```python
plt = gui.new().new().new(gui.available_widgets.Waveform)
plt.plot(x_name='samx', y_name='bpm4i')
plt.plot(device_x='samx', device_y='bpm4i')
```
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title (`title`), axis labels (`x_label`)
<!-- or limits (`x_lim`). -->
@@ -112,7 +112,7 @@ Let's assume BEC was just started and the `gui` object is available in the clien
```python
dock_area = gui.new()
plt = dock_area.new().new(gui.available_widgets.Waveform)
plt.plot(x_name='samx', y_name='bpm4i')
plt.plot(device_x='samx', device_y='bpm4i')
plt.curves[0].set_color(color="white")
plt.title = '1D Waveform'
```

View File

@@ -47,9 +47,9 @@ heatmap_widget = dock_area.new().new(gui.available_widgets.Heatmap)
# Plot a heatmap with x and y motor positions and z detector signal
heatmap_widget.plot(
x_name='samx', # X-axis motor
y_name='samy', # Y-axis motor
z_name='bpm4i', # Z-axis detector signal
device_x='samx', # X-axis motor
device_y='samy', # Y-axis motor
device_z='bpm4i', # Z-axis detector signal
color_map='plasma'
)
heatmap_widget.title = "Grid Scan - Sample Position vs BPM Intensity"
@@ -66,12 +66,12 @@ heatmap_widget = dock_area.new().new(gui.available_widgets.Heatmap)
# Plot heatmap with specific data entries
heatmap_widget.plot(
x_name='motor1',
y_name='motor2',
z_name='detector1',
x_entry='RBV', # Use readback value for x
y_entry='RBV', # Use readback value for y
z_entry='value', # Use main value for z
device_x='motor1',
device_y='motor2',
device_z='detector1',
signal_x='RBV', # Use readback value for x
signal_y='RBV', # Use readback value for y
signal_z='value', # Use main value for z
color_map='viridis',
reload=True # Force reload of data
)

View File

@@ -32,7 +32,7 @@ dock_area = gui.new()
img_widget = dock_area.new().new(gui.available_widgets.Image)
# Add an ImageWidget to the BECFigure for a 2D detector
img_widget.image(device_name='eiger', device_entry='preview')
img_widget.image(device='eiger', signal='preview')
img_widget.title = "Camera Image - Eiger Detector"
```
@@ -46,7 +46,7 @@ dock_area = gui.new()
img_widget = dock_area.new().new(gui.available_widgets.Image)
# Add an ImageWidget to the BECFigure for a 2D detector
img_widget.image(device_name='waveform', device_entry='data')
img_widget.image(device='waveform', signal='data')
img_widget.title = "Line Detector Data"
# Optional: Set the color map and value range
@@ -84,7 +84,7 @@ The Image Widget can be configured for different detectors by specifying the cor
```python
# For a 2D camera detector
img_widget = fig.image(device_name='eiger', device_entry='preview')
img_widget = fig.image(device='eiger', signal='preview')
img_widget.set_title("Eiger Camera Image")
```
@@ -92,7 +92,7 @@ img_widget.set_title("Eiger Camera Image")
```python
# For a 1D line detector
img_widget = fig.image(device_name='waveform', device_entry='data')
img_widget = fig.image(device='waveform', signal='data')
img_widget.set_title("Line Detector Data")
```

View File

@@ -29,8 +29,8 @@ mm1 = dock_area.new().new(gui.available_widgets.MotorMap)
mm2 = dock_area.new().new(gui.available_widgets.MotorMap)
# Add signals to the MotorMaps
mm1.map(x_name='samx', y_name='samy')
mm2.map(x_name='aptrx', y_name='aptry')
mm1.map(device_x='samx', device_y='samy')
mm2.map(device_x='aptrx', device_y='aptry')
```
## Example 2 - Customizing Motor Map Display
@@ -57,7 +57,7 @@ You can dynamically change the motors being tracked and reset the history of the
mm1.reset_history()
# Change the motors being tracked
mm1.map(x_name='aptrx', y_name='aptry')
mm1.map(device_x='aptrx', device_y='aptry')
```
````

View File

@@ -20,7 +20,7 @@ The 2D scatter plot widget is designed for more complex data visualization. It e
```python
# Add a new dock_area, a new dock and a BECWaveForm to the dock
plt = gui.new().new().new(gui.available_widgets.ScatterWaveform)
plt.plot(x_name='samx', y_name='samy', z_name='bpm4i')
plt.plot(device_x='samx', device_y='samy', device_z='bpm4i')
```

View File

@@ -32,8 +32,8 @@ plt1 = dock_area.new().new('Waveform')
plt2 = gui.my_new_dock_area.new().new(gui.available_widgets.Waveform) # as an alternative example via dynamic name space
# Add signals to the WaveformWidget
plt1.plot(x_name='samx', y_name='bpm4i')
plt2.plot(x_name='samx', y_name='bpm3i')
plt1.plot(device_x='samx', device_y='bpm4i')
plt2.plot(device_x='samx', device_y='bpm3i')
# set axis labels
plt1.title = "Gauss plots vs. samx"
@@ -60,10 +60,10 @@ In addition to the scan curve, you can also add a second curve that fits the sig
```python
# Add a new dock_area, dock and Waveform and plot bpm4i vs samx with a GaussianModel DAP
plt = gui.new().new().new('Waveform')
plt.plot(x_name='samx', y_name='bpm4i', dap="GaussianModel")
plt.plot(device_x='samx', device_y='bpm4i', dap="GaussianModel")
# Add a second curve to the same plot without DAP
plt.plot(x_name='samx', y_name='bpm3a')
plt.plot(device_x='samx', device_y='bpm3a')
# Add DAP to the second curve
plt.add_dap_curve(device_label='bpm3a-bpm3a', dap_name='GaussianModel')

View File

@@ -58,8 +58,8 @@ def test_rpc_add_dock_with_plots_e2e(qtbot, bec_client_lib, connected_client_gui
assert gui._ipython_registry[mm._gui_id].__class__ == MotorMap
mm.map("samx", "samy")
curve = wf.plot(x_name="samx", y_name="bpm4i")
im_item = im.image(device_name="eiger", device_entry="preview")
curve = wf.plot(device_x="samx", device_y="bpm4i")
im_item = im.image(device="eiger", signal="preview")
assert curve.__class__.__name__ == "RPCReference"
assert curve.__class__ == RPCReference

View File

@@ -34,7 +34,7 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
sw = dock_area.new("ScatterWaveform")
mw = dock_area.new("MultiWaveform")
c1 = wf.plot(x_name="samx", y_name="bpm4i")
c1 = wf.plot(device_x="samx", device_y="bpm4i")
# Adding custom curves, removing one and adding it again should not crash
c2 = wf.plot(y=[1, 2, 3], x=[1, 2, 3])
assert c2.object_name == "Curve_0"
@@ -42,9 +42,9 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
c3 = wf.plot(y=[1, 2, 3], x=[1, 2, 3])
assert c3.object_name == "Curve_0"
im.image(device_name="eiger", device_entry="preview")
mm.map(x_name="samx", y_name="samy")
sw.plot(x_name="samx", y_name="samy", z_name="bpm4a")
im.image(device="eiger", signal="preview")
mm.map(device_x="samx", device_y="samy")
sw.plot(device_x="samx", device_y="samy", device_z="bpm4a")
mw.plot(monitor="waveform")
# Adding multiple custom curves sho
@@ -70,8 +70,8 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
# Curve
assert c1._config_dict["signal"] == {
"dap": None,
"name": "bpm4i",
"entry": "bpm4i",
"device": "bpm4i",
"signal": "bpm4i",
"dap_oversample": 1,
}
assert c1._config_dict["source"] == "device"
@@ -90,9 +90,9 @@ def test_rpc_waveform_scan(qtbot, bec_client_lib, connected_client_gui_obj):
wf = dock_area.new("Waveform")
# add 3 different curves to track
wf.plot(x_name="samx", y_name="bpm4i")
wf.plot(x_name="samx", y_name="bpm3a")
wf.plot(x_name="samx", y_name="bpm4d")
wf.plot(device_x="samx", device_y="bpm4i")
wf.plot(device_x="samx", device_y="bpm3a")
wf.plot(device_x="samx", device_y="bpm4d")
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
status.wait()
@@ -133,7 +133,7 @@ def test_async_plotting(qtbot, bec_client_lib, connected_client_gui_obj):
dev.waveform.async_update.set("add").wait()
dev.waveform.waveform_shape.set(10000).wait()
wf = dock_area.new("Waveform")
curve = wf.plot(y_name="waveform")
curve = wf.plot(device_y="waveform")
status = scans.line_scan(dev.samx, -5, 5, steps=5, exp_time=0.05, relative=False)
status.wait()
@@ -165,7 +165,7 @@ def test_rpc_image(qtbot, bec_client_lib, connected_client_gui_obj):
scans = client.scans
im = dock_area.new("Image")
im.image(device_name="eiger", device_entry="preview")
im.image(device="eiger", signal="preview")
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
status.wait()
@@ -188,7 +188,7 @@ def test_rpc_motor_map(qtbot, bec_client_lib, connected_client_gui_obj):
dock_area = gui.bec
motor_map = dock_area.new("MotorMap")
motor_map.map(x_name="samx", y_name="samy")
motor_map.map(device_x="samx", device_y="samy")
initial_pos_x = dev.samx.read()["samx"]["value"]
initial_pos_y = dev.samy.read()["samy"]["value"]
@@ -219,7 +219,7 @@ def test_dap_rpc(qtbot, bec_client_lib, connected_client_gui_obj):
dock_area = gui.bec
wf = dock_area.new("Waveform")
wf.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
wf.plot(device_x="samx", device_y="bpm4i", dap="GaussianModel")
dev.bpm4i.sim.select_model("GaussianModel")
params = dev.bpm4i.sim.params
@@ -262,7 +262,7 @@ def test_waveform_passing_device(qtbot, bec_client_lib, connected_client_gui_obj
wf = dock_area.new("Waveform")
c1 = wf.plot(
y_name=dev.samx, y_entry=dev.samx.setpoint
device_y=dev.samx, signal_y=dev.samx.setpoint
) # using setpoint to not use readback signal
assert c1.object_name == "samx_samx_setpoint"
@@ -342,7 +342,7 @@ def test_rpc_waveform_history_curve(
# Add curve from history using the chosen selector; single curve per scan to avoid duplicates
kwargs = {history_selector: sel_value}
curve = wf.plot(x_name="samx", y_name="bpm4i", **kwargs)
curve = wf.plot(device_x="samx", device_y="bpm4i", **kwargs)
num_elements = 10

View File

@@ -12,19 +12,19 @@ def test_rpc_reference_objects(connected_client_gui_obj):
dock_area = gui.window_list[0]
plt = dock_area.new("Waveform", object_name="fig")
plt.plot(x_name="samx", y_name="bpm4i")
plt.plot(device_x="samx", device_y="bpm4i")
im = dock_area.new("Image")
im.image(device_name="eiger", device_entry="preview")
im.image(device="eiger", signal="preview")
motor_map = dock_area.new("MotorMap")
motor_map.map("samx", "samy")
plt_z = dock_area.new("Waveform")
plt_z.plot(x_name="samx", y_name="samy", z_name="bpm4i")
plt_z.plot(device_x="samx", device_y="samy", device_z="bpm4i")
assert len(plt_z.curves) == 1
assert len(plt.curves) == 1
assert im.device_name == "eiger"
assert im.device_entry == "preview"
assert im.device == "eiger"
assert im.signal == "preview"
assert isinstance(im.main_image, RPCReference)
image_item = gui._ipython_registry.get(im.main_image._gui_id, None)

View File

@@ -234,7 +234,7 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
scans = bec.scans
dev = bec.device_manager.devices
# Test rpc calls
img = widget.image(device_name=dev.eiger.name, device_entry="preview")
img = widget.image(device=dev.eiger.name, signal="preview")
assert img.get_data() is None
# Run a scan and plot the image
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
@@ -254,7 +254,7 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
assert np.allclose(img.get_data(), last_img)
# Now add a device with a preview signal
img = widget.image(device_name="eiger", device_entry="preview")
img = widget.image(device="eiger", signal="preview")
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
s.wait()

View File

@@ -179,15 +179,15 @@ def test_add_new_curve(curve_tree_fixture):
assert curve_tree.tree.topLevelItemCount() == 0
with patch.object(curve_tree, "_ensure_color_buffer_size") as ensure_spy:
new_item = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
new_item = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
ensure_spy.assert_called_once()
assert curve_tree.tree.topLevelItemCount() == 1
last_item = curve_tree.all_items[-1]
assert last_item is new_item
assert new_item.config.source == "device"
assert new_item.config.signal.name == "bpm4i"
assert new_item.config.signal.entry == "bpm4i"
assert new_item.config.signal.device == "bpm4i"
assert new_item.config.signal.signal == "bpm4i"
assert new_item.config.color in curve_tree.color_buffer
@@ -197,8 +197,8 @@ def test_renormalize_colors(curve_tree_fixture):
"""
curve_tree, wf = curve_tree_fixture
# Add multiple curves
c1 = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
c2 = curve_tree.add_new_curve(name="bpm3a", entry="bpm3a")
c1 = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
c2 = curve_tree.add_new_curve(device="bpm3a", signal="bpm3a")
curve_tree.color_buffer = []
set_color_spy_c1 = patch.object(c1.color_button, "set_color")
@@ -215,7 +215,7 @@ def test_expand_collapse(curve_tree_fixture):
Test expand_all_daps() and collapse_all_daps() calls expand/collapse on every top-level item.
"""
curve_tree, wf = curve_tree_fixture
c1 = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
c1 = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
curve_tree.tree.expandAll()
expand_spy = patch.object(curve_tree.tree, "expandItem")
collapse_spy = patch.object(curve_tree.tree, "collapseItem")
@@ -236,8 +236,8 @@ def test_send_curve_json(curve_tree_fixture, monkeypatch):
"""
curve_tree, wf = curve_tree_fixture
# Add multiple curves
curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
curve_tree.add_new_curve(name="bpm3a", entry="bpm3a")
curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
curve_tree.add_new_curve(device="bpm3a", signal="bpm3a")
curve_tree.color_palette = "viridis"
curve_tree.send_curve_json()
@@ -282,7 +282,7 @@ def test_add_dap_row(curve_tree_fixture):
curve_tree, wf = curve_tree_fixture
# Add a device curve first
device_row = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
device_row = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
assert device_row.source == "device"
assert curve_tree.tree.topLevelItemCount() == 1
assert device_row.childCount() == 0
@@ -299,8 +299,8 @@ def test_add_dap_row(curve_tree_fixture):
assert dap_child.config.parent_label == device_row.config.label
# Check that the DAP inherits device name/entry from parent
assert dap_child.config.signal.name == "bpm4i"
assert dap_child.config.signal.entry == "bpm4i"
assert dap_child.config.signal.device == "bpm4i"
assert dap_child.config.signal.signal == "bpm4i"
# Check that the item is in the curve_tree's all_items list
assert dap_child in curve_tree.all_items
@@ -313,8 +313,8 @@ def test_remove_self_top_level(curve_tree_fixture):
curve_tree, wf = curve_tree_fixture
# Add two device curves
row1 = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
row2 = curve_tree.add_new_curve(name="bpm3a", entry="bpm3a")
row1 = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
row2 = curve_tree.add_new_curve(device="bpm3a", signal="bpm3a")
assert curve_tree.tree.topLevelItemCount() == 2
assert len(curve_tree.all_items) == 2
@@ -335,7 +335,7 @@ def test_remove_self_child(curve_tree_fixture):
curve_tree, wf = curve_tree_fixture
# Add a device curve and a DAP child
device_row = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
device_row = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
device_row.add_dap_row()
dap_child = device_row.child(0)
@@ -360,7 +360,7 @@ def test_export_data_dap(curve_tree_fixture):
curve_tree, wf = curve_tree_fixture
# Add a device curve with specific parameters
device_row = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
device_row = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
# Add a DAP child
device_row.add_dap_row()
@@ -375,8 +375,8 @@ def test_export_data_dap(curve_tree_fixture):
# Check the exported data
assert exported["source"] == "dap"
assert exported["parent_label"] == "bpm4i-bpm4i"
assert exported["signal"]["name"] == "bpm4i"
assert exported["signal"]["entry"] == "bpm4i"
assert exported["signal"]["device"] == "bpm4i"
assert exported["signal"]["signal"] == "bpm4i"
assert exported["signal"]["dap"] == "GaussianModel"
assert exported["label"] == "bpm4i-bpm4i-GaussianModel"
@@ -422,7 +422,7 @@ def test_export_data_history_curve(curve_tree_fixture, scan_history_factory):
wf.client.queue.scan_storage.current_scan = None
# Create a device row and select scan index "2"
device_row = curve_tree.add_new_curve(name="bpm4i", entry="bpm4i")
device_row = curve_tree.add_new_curve(device="bpm4i", signal="bpm4i")
device_row.scan_index_combo.setCurrentText("2")
exported = device_row.export_data()

View File

@@ -30,11 +30,11 @@ def heatmap_widget(qtbot, mocked_client):
def test_heatmap_plot(heatmap_widget):
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
assert heatmap_widget._image_config.x_device.name == "samx"
assert heatmap_widget._image_config.y_device.name == "samy"
assert heatmap_widget._image_config.z_device.name == "bpm4i"
assert heatmap_widget._image_config.device_x.device == "samx"
assert heatmap_widget._image_config.device_y.device == "samy"
assert heatmap_widget._image_config.device_z.device == "bpm4i"
def test_heatmap_on_scan_status_no_scan_id(heatmap_widget):
@@ -78,7 +78,7 @@ def test_heatmap_get_image_data_grid_scan(heatmap_widget):
info={},
request_inputs={"arg_bundle": ["samx", -5, 5, 10, "samy", -5, 5, 10], "kwargs": {}},
)
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
heatmap_widget.status_message = scan_msg
with mock.patch.object(heatmap_widget, "get_grid_scan_image") as mock_get_grid_scan_image:
@@ -147,9 +147,9 @@ def test_heatmap_get_grid_scan_image(heatmap_widget):
)
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
img, _ = heatmap_widget.get_grid_scan_image(list(range(100)), msg=scan_msg)
@@ -174,9 +174,9 @@ def _grid_positions(
def test_heatmap_grid_scan_direction_and_snaking_x_fast(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
@@ -219,9 +219,9 @@ def test_heatmap_grid_scan_direction_and_snaking_x_fast(heatmap_widget):
def test_heatmap_grid_scan_direction_and_snaking_y_fast(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
@@ -277,13 +277,13 @@ def test_heatmap_get_step_scan_image(heatmap_widget):
heatmap_widget.scan_item.status_message = scan_msg
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
img, _ = heatmap_widget.get_step_scan_image(
list(np.random.rand(100)), list(np.random.rand(100)), list(range(100)), msg=scan_msg
list(np.random.rand(100)), list(np.random.rand(100)), list(range(100))
)
assert img.shape > (10, 10)
@@ -291,9 +291,9 @@ def test_heatmap_get_step_scan_image(heatmap_widget):
def test_heatmap_update_plot_no_scan_item(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
with mock.patch.object(heatmap_widget.main_image, "setImage") as mock_set_image:
@@ -304,9 +304,9 @@ def test_heatmap_update_plot_no_scan_item(heatmap_widget):
def test_heatmap_update_plot(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
heatmap_widget.scan_item = create_dummy_scan_item()
@@ -331,9 +331,9 @@ def test_heatmap_update_plot(heatmap_widget):
def test_heatmap_update_plot_without_status_message(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
heatmap_widget.scan_item = create_dummy_scan_item()
@@ -346,9 +346,9 @@ def test_heatmap_update_plot_without_status_message(heatmap_widget):
def test_heatmap_update_plot_no_img_data(heatmap_widget):
heatmap_widget._image_config = HeatmapConfig(
parent_id="parent_id",
x_device=HeatmapDeviceSignal(name="samx", entry="samx"),
y_device=HeatmapDeviceSignal(name="samy", entry="samy"),
z_device=HeatmapDeviceSignal(name="bpm4i", entry="bpm4i"),
device_x=HeatmapDeviceSignal(device="samx", signal="samx"),
device_y=HeatmapDeviceSignal(device="samy", signal="samy"),
device_z=HeatmapDeviceSignal(device="bpm4i", signal="bpm4i"),
color_map="viridis",
)
heatmap_widget.scan_item = create_dummy_scan_item()
@@ -407,7 +407,7 @@ def test_heatmap_settings_popup_accept_changes(heatmap_widget, qtbot):
"""
Test that changes made in the settings dialog are applied correctly.
"""
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
assert heatmap_widget.color_map == "plasma" # Default colormap
heatmap_widget.show_heatmap_settings()
qtbot.waitUntil(lambda: heatmap_widget.heatmap_dialog is not None)
@@ -431,7 +431,7 @@ def test_heatmap_settings_popup_show_settings(heatmap_widget, qtbot):
"""
Test that the settings dialog opens and contains the expected elements.
"""
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
heatmap_widget.show_heatmap_settings()
qtbot.waitUntil(lambda: heatmap_widget.heatmap_dialog is not None)
@@ -439,13 +439,13 @@ def test_heatmap_settings_popup_show_settings(heatmap_widget, qtbot):
assert dialog.isVisible()
assert dialog.widget is not None
assert hasattr(dialog.widget.ui, "color_map")
assert hasattr(dialog.widget.ui, "x_name")
assert hasattr(dialog.widget.ui, "y_name")
assert hasattr(dialog.widget.ui, "z_name")
assert hasattr(dialog.widget.ui, "device_x")
assert hasattr(dialog.widget.ui, "device_y")
assert hasattr(dialog.widget.ui, "device_z")
# Check that the ui elements are correctly initialized
assert dialog.widget.ui.color_map.colormap == heatmap_widget.color_map
assert dialog.widget.ui.x_name.currentText() == heatmap_widget._image_config.x_device.name
assert dialog.widget.ui.device_x.currentText() == heatmap_widget._image_config.device_x.device
dialog.reject()
qtbot.waitUntil(lambda: heatmap_widget.heatmap_dialog is None)
@@ -458,7 +458,7 @@ def test_heatmap_widget_reset(heatmap_widget):
heatmap_widget._pending_interpolation_request = object()
heatmap_widget._latest_interpolation_version = 5
heatmap_widget.scan_item = create_dummy_scan_item()
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
heatmap_widget.reset()
assert heatmap_widget._grid_index is None
@@ -476,12 +476,12 @@ def test_heatmap_widget_update_plot_with_scan_history(heatmap_widget, grid_scan_
heatmap_widget.client.history._scan_ids.append(grid_scan_history_msg.scan_id)
heatmap_widget.client.queue.scan_storage.current_scan = None
heatmap_widget.plot(
x_name="samx",
y_name="samy",
z_name="bpm4i",
x_entry="samx",
y_entry="samy",
z_entry="bpm4i",
device_x="samx",
device_y="samy",
device_z="bpm4i",
signal_x="samx",
signal_y="samy",
signal_z="bpm4i",
)
qtbot.waitUntil(lambda: heatmap_widget.main_image.raw_data is not None)
qtbot.waitUntil(lambda: heatmap_widget.main_image.raw_data.shape == (10, 10))
@@ -602,219 +602,219 @@ def test_finish_interpolation_thread_cleans_references(heatmap_widget):
def test_device_safe_properties_get(heatmap_widget):
"""Test that device SafeProperty getters work correctly."""
# Initially devices should be empty
assert heatmap_widget.x_device_name == ""
assert heatmap_widget.x_device_entry == ""
assert heatmap_widget.y_device_name == ""
assert heatmap_widget.y_device_entry == ""
assert heatmap_widget.z_device_name == ""
assert heatmap_widget.z_device_entry == ""
assert heatmap_widget.device_x == ""
assert heatmap_widget.signal_x == ""
assert heatmap_widget.device_y == ""
assert heatmap_widget.signal_y == ""
assert heatmap_widget.device_z == ""
assert heatmap_widget.signal_z == ""
# Set devices via plot
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Check properties return device names and entries separately
assert heatmap_widget.x_device_name == "samx"
assert heatmap_widget.x_device_entry # Should have some entry
assert heatmap_widget.y_device_name == "samy"
assert heatmap_widget.y_device_entry # Should have some entry
assert heatmap_widget.z_device_name == "bpm4i"
assert heatmap_widget.z_device_entry # Should have some entry
assert heatmap_widget.device_x == "samx"
assert heatmap_widget.signal_x # Should have some entry
assert heatmap_widget.device_y == "samy"
assert heatmap_widget.signal_y # Should have some entry
assert heatmap_widget.device_z == "bpm4i"
assert heatmap_widget.signal_z # Should have some entry
def test_device_safe_properties_set_name(heatmap_widget):
"""Test that device SafeProperty setters work for device names."""
# Set x_device_name - should auto-validate entry
heatmap_widget.x_device_name = "samx"
assert heatmap_widget._image_config.x_device is not None
assert heatmap_widget._image_config.x_device.name == "samx"
assert heatmap_widget._image_config.x_device.entry is not None # Entry should be validated
assert heatmap_widget.x_device_name == "samx"
# Set device_x - should auto-validate entry
heatmap_widget.device_x = "samx"
assert heatmap_widget._image_config.device_x is not None
assert heatmap_widget._image_config.device_x.device == "samx"
assert heatmap_widget._image_config.device_x.signal is not None # Entry should be validated
assert heatmap_widget.device_x == "samx"
# Set y_device_name
heatmap_widget.y_device_name = "samy"
assert heatmap_widget._image_config.y_device is not None
assert heatmap_widget._image_config.y_device.name == "samy"
assert heatmap_widget._image_config.y_device.entry is not None
assert heatmap_widget.y_device_name == "samy"
# Set device_y
heatmap_widget.device_y = "samy"
assert heatmap_widget._image_config.device_y is not None
assert heatmap_widget._image_config.device_y.device == "samy"
assert heatmap_widget._image_config.device_y.signal is not None
assert heatmap_widget.device_y == "samy"
# Set z_device_name
heatmap_widget.z_device_name = "bpm4i"
assert heatmap_widget._image_config.z_device is not None
assert heatmap_widget._image_config.z_device.name == "bpm4i"
assert heatmap_widget._image_config.z_device.entry is not None
assert heatmap_widget.z_device_name == "bpm4i"
# Set device_z
heatmap_widget.device_z = "bpm4i"
assert heatmap_widget._image_config.device_z is not None
assert heatmap_widget._image_config.device_z.device == "bpm4i"
assert heatmap_widget._image_config.device_z.signal is not None
assert heatmap_widget.device_z == "bpm4i"
def test_device_safe_properties_set_entry(heatmap_widget):
"""Test that device entry properties can override default entries."""
# Set device name first - this auto-validates entry
heatmap_widget.x_device_name = "samx"
initial_entry = heatmap_widget.x_device_entry
heatmap_widget.device_x = "samx"
initial_entry = heatmap_widget.signal_x
assert initial_entry # Should have auto-validated entry
# Override with specific entry
heatmap_widget.x_device_entry = "samx"
assert heatmap_widget._image_config.x_device.entry == "samx"
assert heatmap_widget.x_device_entry == "samx"
heatmap_widget.signal_x = "samx"
assert heatmap_widget._image_config.device_x.signal == "samx"
assert heatmap_widget.signal_x == "samx"
# Same for y device
heatmap_widget.y_device_name = "samy"
heatmap_widget.y_device_entry = "samy_setpoint"
assert heatmap_widget._image_config.y_device.entry == "samy_setpoint"
heatmap_widget.device_y = "samy"
heatmap_widget.signal_y = "samy_setpoint"
assert heatmap_widget._image_config.device_y.signal == "samy_setpoint"
# Same for z device
heatmap_widget.z_device_name = "bpm4i"
heatmap_widget.z_device_entry = "bpm4i"
assert heatmap_widget._image_config.z_device.entry == "bpm4i"
heatmap_widget.device_z = "bpm4i"
heatmap_widget.signal_z = "bpm4i"
assert heatmap_widget._image_config.device_z.signal == "bpm4i"
def test_device_entry_cannot_be_set_without_name(heatmap_widget):
"""Test that setting entry without device name logs warning and does nothing."""
# Try to set entry without device name
heatmap_widget.x_device_entry = "some_entry"
heatmap_widget.signal_x = "some_entry"
# Should not crash, entry should remain empty
assert heatmap_widget.x_device_entry == ""
assert heatmap_widget._image_config.x_device is None
assert heatmap_widget.signal_x == ""
assert heatmap_widget._image_config.device_x is None
def test_device_safe_properties_set_empty(heatmap_widget):
"""Test that device SafeProperty setters handle empty strings."""
# Set device first
heatmap_widget.x_device_name = "samx"
assert heatmap_widget._image_config.x_device is not None
heatmap_widget.device_x = "samx"
assert heatmap_widget._image_config.device_x is not None
# Set to empty string - should clear the device
heatmap_widget.x_device_name = ""
assert heatmap_widget.x_device_name == ""
assert heatmap_widget._image_config.x_device is None
heatmap_widget.device_x = ""
assert heatmap_widget.device_x == ""
assert heatmap_widget._image_config.device_x is None
def test_device_safe_properties_auto_plot(heatmap_widget):
"""Test that setting all three devices triggers auto-plot."""
# Set all three devices
heatmap_widget.x_device_name = "samx"
heatmap_widget.y_device_name = "samy"
heatmap_widget.z_device_name = "bpm4i"
heatmap_widget.device_x = "samx"
heatmap_widget.device_y = "samy"
heatmap_widget.device_z = "bpm4i"
# Check that plot was called (image_config should be updated)
assert heatmap_widget._image_config.x_device is not None
assert heatmap_widget._image_config.y_device is not None
assert heatmap_widget._image_config.z_device is not None
assert heatmap_widget._image_config.device_x is not None
assert heatmap_widget._image_config.device_y is not None
assert heatmap_widget._image_config.device_z is not None
def test_device_properties_update_labels(heatmap_widget):
"""Test that setting device properties updates axis labels."""
# Set x device - should update x label
heatmap_widget.x_device_name = "samx"
heatmap_widget.device_x = "samx"
assert heatmap_widget.x_label == "samx"
# Set y device - should update y label
heatmap_widget.y_device_name = "samy"
heatmap_widget.device_y = "samy"
assert heatmap_widget.y_label == "samy"
# Set z device - should update title
heatmap_widget.z_device_name = "bpm4i"
heatmap_widget.device_z = "bpm4i"
assert heatmap_widget.title == "bpm4i"
def test_device_properties_partial_configuration(heatmap_widget):
"""Test that widget handles partial device configuration gracefully."""
# Set only x device
heatmap_widget.x_device_name = "samx"
assert heatmap_widget.x_device_name == "samx"
assert heatmap_widget.y_device_name == ""
assert heatmap_widget.z_device_name == ""
heatmap_widget.device_x = "samx"
assert heatmap_widget.device_x == "samx"
assert heatmap_widget.device_y == ""
assert heatmap_widget.device_z == ""
# Set only y device (x already set)
heatmap_widget.y_device_name = "samy"
assert heatmap_widget.x_device_name == "samx"
assert heatmap_widget.y_device_name == "samy"
assert heatmap_widget.z_device_name == ""
heatmap_widget.device_y = "samy"
assert heatmap_widget.device_x == "samx"
assert heatmap_widget.device_y == "samy"
assert heatmap_widget.device_z == ""
# Auto-plot should not trigger yet (z missing)
# But devices should be configured
assert heatmap_widget._image_config.x_device is not None
assert heatmap_widget._image_config.y_device is not None
assert heatmap_widget._image_config.device_x is not None
assert heatmap_widget._image_config.device_y is not None
def test_device_properties_in_user_access(heatmap_widget):
"""Test that device properties are exposed in USER_ACCESS for RPC."""
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
assert "x_device_name" in Heatmap.USER_ACCESS
assert "x_device_name.setter" in Heatmap.USER_ACCESS
assert "x_device_entry" in Heatmap.USER_ACCESS
assert "x_device_entry.setter" in Heatmap.USER_ACCESS
assert "y_device_name" in Heatmap.USER_ACCESS
assert "y_device_name.setter" in Heatmap.USER_ACCESS
assert "y_device_entry" in Heatmap.USER_ACCESS
assert "y_device_entry.setter" in Heatmap.USER_ACCESS
assert "z_device_name" in Heatmap.USER_ACCESS
assert "z_device_name.setter" in Heatmap.USER_ACCESS
assert "z_device_entry" in Heatmap.USER_ACCESS
assert "z_device_entry.setter" in Heatmap.USER_ACCESS
assert "device_x" in Heatmap.USER_ACCESS
assert "device_x.setter" in Heatmap.USER_ACCESS
assert "signal_x" in Heatmap.USER_ACCESS
assert "signal_x.setter" in Heatmap.USER_ACCESS
assert "device_y" in Heatmap.USER_ACCESS
assert "device_y.setter" in Heatmap.USER_ACCESS
assert "signal_y" in Heatmap.USER_ACCESS
assert "signal_y.setter" in Heatmap.USER_ACCESS
assert "device_z" in Heatmap.USER_ACCESS
assert "device_z.setter" in Heatmap.USER_ACCESS
assert "signal_z" in Heatmap.USER_ACCESS
assert "signal_z.setter" in Heatmap.USER_ACCESS
def test_device_properties_validation(heatmap_widget):
"""Test that device entries are validated through entry_validator."""
# Set device name - entry should be auto-validated
heatmap_widget.x_device_name = "samx"
initial_entry = heatmap_widget.x_device_entry
heatmap_widget.device_x = "samx"
initial_entry = heatmap_widget.signal_x
# The entry should be validated (will be "samx" in the mock)
assert initial_entry == "samx"
# Set a different entry - should also be validated
heatmap_widget.x_device_entry = "samx" # Use same name as validated entry
assert heatmap_widget.x_device_entry == "samx"
heatmap_widget.signal_x = "samx" # Use same name as validated entry
assert heatmap_widget.signal_x == "samx"
def test_device_properties_with_plot_method(heatmap_widget):
"""Test that device properties reflect values set via plot() method."""
# Use plot method
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Properties should reflect the plotted devices
assert heatmap_widget.x_device_name == "samx"
assert heatmap_widget.y_device_name == "samy"
assert heatmap_widget.z_device_name == "bpm4i"
assert heatmap_widget.device_x == "samx"
assert heatmap_widget.device_y == "samy"
assert heatmap_widget.device_z == "bpm4i"
# Entries should be validated
assert heatmap_widget.x_device_entry == "samx"
assert heatmap_widget.y_device_entry == "samy"
assert heatmap_widget.z_device_entry == "bpm4i"
assert heatmap_widget.signal_x == "samx"
assert heatmap_widget.signal_y == "samy"
assert heatmap_widget.signal_z == "bpm4i"
def test_device_properties_overwrite_via_properties(heatmap_widget):
"""Test that device properties can overwrite values set via plot()."""
# First set via plot
heatmap_widget.plot(x_name="samx", y_name="samy", z_name="bpm4i")
heatmap_widget.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Overwrite x device via properties
heatmap_widget.x_device_name = "samz"
assert heatmap_widget.x_device_name == "samz"
assert heatmap_widget._image_config.x_device.name == "samz"
heatmap_widget.device_x = "samz"
assert heatmap_widget.device_x == "samz"
assert heatmap_widget._image_config.device_x.device == "samz"
# Overwrite y device entry
heatmap_widget.y_device_entry = "samy"
assert heatmap_widget.y_device_entry == "samy"
heatmap_widget.signal_y = "samy"
assert heatmap_widget.signal_y == "samy"
def test_device_properties_clearing_devices(heatmap_widget):
"""Test clearing devices by setting to empty string."""
# Set all devices
heatmap_widget.x_device_name = "samx"
heatmap_widget.y_device_name = "samy"
heatmap_widget.z_device_name = "bpm4i"
heatmap_widget.device_x = "samx"
heatmap_widget.device_y = "samy"
heatmap_widget.device_z = "bpm4i"
# Clear x device
heatmap_widget.x_device_name = ""
assert heatmap_widget.x_device_name == ""
assert heatmap_widget._image_config.x_device is None
heatmap_widget.device_x = ""
assert heatmap_widget.device_x == ""
assert heatmap_widget._image_config.device_x is None
# Y and Z should still be set
assert heatmap_widget.y_device_name == "samy"
assert heatmap_widget.z_device_name == "bpm4i"
assert heatmap_widget.device_y == "samy"
assert heatmap_widget.device_z == "bpm4i"
def test_device_properties_property_changed_signal(heatmap_widget):
@@ -826,12 +826,12 @@ def test_device_properties_property_changed_signal(heatmap_widget):
heatmap_widget.property_changed.connect(mock_handler)
# Set device name
heatmap_widget.x_device_name = "samx"
heatmap_widget.device_x = "samx"
# Signal should have been emitted
assert mock_handler.called
# Check it was called with correct arguments
mock_handler.assert_any_call("x_device_name", "samx")
mock_handler.assert_any_call("device_x", "samx")
def test_auto_emit_syncs_heatmap_toolbar_actions(heatmap_widget):
@@ -855,7 +855,7 @@ def test_auto_emit_syncs_heatmap_toolbar_actions(heatmap_widget):
def test_device_entry_validation_with_invalid_device(heatmap_widget):
"""Test that invalid device names are handled gracefully."""
# Try to set invalid device name
heatmap_widget.x_device_name = "nonexistent_device"
heatmap_widget.device_x = "nonexistent_device"
# Should not crash, but device might not be set if validation fails
# The implementation silently fails, so we just check it doesn't crash
@@ -864,28 +864,28 @@ def test_device_entry_validation_with_invalid_device(heatmap_widget):
def test_device_properties_sequential_entry_changes(heatmap_widget):
"""Test changing device entry multiple times."""
# Set device
heatmap_widget.x_device_name = "samx"
heatmap_widget.device_x = "samx"
# Change entry multiple times
heatmap_widget.x_device_entry = "samx_velocity"
assert heatmap_widget.x_device_entry == "samx_velocity"
heatmap_widget.signal_x = "samx_velocity"
assert heatmap_widget.signal_x == "samx_velocity"
heatmap_widget.x_device_entry = "samx_setpoint"
assert heatmap_widget.x_device_entry == "samx_setpoint"
heatmap_widget.signal_x = "samx_setpoint"
assert heatmap_widget.signal_x == "samx_setpoint"
heatmap_widget.x_device_entry = "samx"
assert heatmap_widget.x_device_entry == "samx"
heatmap_widget.signal_x = "samx"
assert heatmap_widget.signal_x == "samx"
def test_device_properties_with_none_values(heatmap_widget):
"""Test that None values are handled as empty strings."""
# Device name None should be treated as empty
heatmap_widget.x_device_name = None
assert heatmap_widget.x_device_name == ""
heatmap_widget.device_x = None
assert heatmap_widget.device_x == ""
# Set a device first
heatmap_widget.y_device_name = "samy"
heatmap_widget.device_y = "samy"
# Entry None should not change anything
heatmap_widget.y_device_entry = None
assert heatmap_widget.y_device_entry # Should still have validated entry
heatmap_widget.signal_y = None
assert heatmap_widget.signal_y # Should still have validated entry

View File

@@ -14,14 +14,9 @@ from tests.unit_tests.conftest import create_widget
def _set_signal_config(
client,
device_name: str,
signal_name: str,
signal_class: str,
ndim: int,
obj_name: str | None = None,
client, device: str, signal_name: str, signal_class: str, ndim: int, obj_name: str | None = None
):
device = client.device_manager.devices[device_name]
device = client.device_manager.devices[device]
device._info["signals"][signal_name] = {
"obj_name": obj_name or signal_name,
"signal_class": signal_class,
@@ -153,14 +148,14 @@ def test_image_setup_preview_signal_1d(qtbot, mocked_client):
obj_name="waveform1d_img",
)
view.image(device_name="waveform1d", device_entry="img")
view.image(device="waveform1d", signal="img")
# Subscriptions should indicate 1D preview connection
sub = view.subscriptions["main"]
assert sub.source == "device_monitor_1d"
assert sub.monitor_type == "1d"
assert view.device_name == "waveform1d"
assert view.device_entry == "img"
assert view.device == "waveform1d"
assert view.signal == "img"
# Simulate a waveform update from the dispatcher
waveform = np.arange(25, dtype=float)
@@ -187,14 +182,14 @@ def test_image_setup_preview_signal_2d(qtbot, mocked_client):
obj_name="eiger_img2d",
)
view.image(device_name="eiger", device_entry="img2d")
view.image(device="eiger", signal="img2d")
# Subscriptions should indicate 2D preview connection
sub = view.subscriptions["main"]
assert sub.source == "device_monitor_2d"
assert sub.monitor_type == "2d"
assert view.device_name == "eiger"
assert view.device_entry == "img2d"
assert view.device == "eiger"
assert view.signal == "img2d"
# Simulate a 2D image update
test_data = np.arange(16, dtype=float).reshape(4, 4)
@@ -259,7 +254,7 @@ def test_image_async_signal_uses_obj_name(qtbot, mocked_client, monkeypatch):
mocked_client, "eiger", "img", signal_class="AsyncSignal", ndim=1, obj_name="async_obj"
)
view.image(device_name="eiger", device_entry="img")
view.image(device="eiger", signal="img")
assert view.subscriptions["main"].async_signal_name == "async_obj"
assert view.async_update is True
@@ -300,7 +295,7 @@ def test_disconnect_clears_async_state(qtbot, mocked_client, monkeypatch):
mocked_client, "eiger", "img", signal_class="AsyncSignal", ndim=2, obj_name="async_obj"
)
view.image(device_name="eiger", device_entry="img")
view.image(device="eiger", signal="img")
view.scan_id = "scan_x"
view.old_scan_id = "scan_y"
view.subscriptions["main"].async_signal_name = "async_obj"
@@ -308,7 +303,7 @@ def test_disconnect_clears_async_state(qtbot, mocked_client, monkeypatch):
# Avoid touching real dispatcher
monkeypatch.setattr(view.bec_dispatcher, "disconnect_slot", lambda *args, **kwargs: None)
view.disconnect_monitor(device_name="eiger", device_entry="img")
view.disconnect_monitor(device="eiger", signal="img")
assert view.subscriptions["main"].async_signal_name is None
assert view.async_update is False
@@ -322,7 +317,7 @@ def test_image_setup_rejects_unsupported_signal_class(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img", signal_class="Signal", ndim=2)
view.image(device_name="eiger", device_entry="img")
view.image(device="eiger", signal="img")
assert view.subscriptions["main"].source is None
assert view.subscriptions["main"].monitor_type is None
@@ -333,13 +328,13 @@ def test_image_disconnects_with_missing_entry(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img", signal_class="PreviewSignal", ndim=2)
view.image(device_name="eiger", device_entry="img")
assert view.device_name == "eiger"
assert view.device_entry == "img"
view.image(device="eiger", signal="img")
assert view.device == "eiger"
assert view.signal == "img"
view.image(device_name="eiger", device_entry=None)
assert view.device_name == ""
assert view.device_entry == ""
view.image(device="eiger", signal=None)
assert view.device == ""
assert view.signal == ""
def test_handle_scan_change_clears_buffers_and_resets_crosshair(qtbot, mocked_client, monkeypatch):
@@ -541,8 +536,8 @@ def test_setup_image_from_toolbar(qtbot, mocked_client, monkeypatch):
bec_image_view.on_device_selection_changed(None)
qtbot.wait(200)
assert bec_image_view.device_name == "eiger"
assert bec_image_view.device_entry == "img"
assert bec_image_view.device == "eiger"
assert bec_image_view.signal == "img"
assert bec_image_view.subscriptions["main"].source == "device_monitor_2d"
assert bec_image_view.subscriptions["main"].monitor_type == "2d"
assert bec_image_view.main_image.raw_data is None
@@ -834,8 +829,8 @@ def test_device_selection_syncs_from_properties(qtbot, mocked_client, monkeypatc
),
)
view.device_name = "eiger"
view.device_entry = "img2d"
view.device = "eiger"
view.signal = "img2d"
qtbot.wait(200) # Allow signal processing
@@ -847,19 +842,19 @@ def test_device_selection_syncs_from_properties(qtbot, mocked_client, monkeypatc
)
def test_device_entry_syncs_from_toolbar(qtbot, mocked_client):
def test_signal_syncs_from_toolbar(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img_a", signal_class="PreviewSignal", ndim=2)
_set_signal_config(mocked_client, "eiger", "img_b", signal_class="PreviewSignal", ndim=2)
view.device_name = "eiger"
view.device_entry = "img_a"
view.device = "eiger"
view.signal = "img_a"
device_selection = view.toolbar.components.get_action("device_selection").widget
device_selection.signal_combo_box.blockSignals(True)
device_selection.signal_combo_box.setCurrentText("img_b")
device_selection.signal_combo_box.blockSignals(False)
view._sync_device_entry_from_toolbar()
view._sync_signal_from_toolbar()
assert view.device_entry == "img_b"
assert view.signal == "img_b"

View File

@@ -23,12 +23,12 @@ def test_motor_map_select_motor(qtbot, mocked_client):
"""Test selecting motors for the motor map."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy", validate_bec=True)
mm.map(device_x="samx", device_y="samy", validate_bec=True)
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samy"
assert mm.config.x_motor.limits == [-10, 10]
assert mm.config.y_motor.limits == [-5, 5]
assert mm.config.device_x.device == "samx"
assert mm.config.device_y.device == "samy"
assert mm.config.device_x.limits == [-10, 10]
assert mm.config.device_y.limits == [-5, 5]
assert mm.config.scatter_size == 5
assert mm.config.max_points == 5000
assert mm.config.num_dim_points == 100
@@ -39,7 +39,7 @@ def test_motor_map_select_motor(qtbot, mocked_client):
def test_motor_map_properties(qtbot, mocked_client):
"""Test setting and getting properties of MotorMap."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
# Test color property
mm.color = (100, 150, 200, 255)
@@ -86,7 +86,7 @@ def test_motor_map_properties(qtbot, mocked_client):
def test_motor_map_get_limits(qtbot, mocked_client):
"""Test getting motor limits."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
expected_limits = {"samx": [-10, 10], "samy": [-5, 5]}
for motor_name, expected_limit in expected_limits.items():
@@ -133,7 +133,7 @@ def test_motor_map_reset_history(qtbot, mocked_client):
def test_motor_map_on_device_readback(qtbot, mocked_client):
"""Test the motor map updates when receiving device readback."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
# Clear the buffer and add initial position
mm._buffer = {"x": [1.0], "y": [2.0]}
@@ -161,7 +161,7 @@ def test_motor_map_on_device_readback(qtbot, mocked_client):
def test_motor_map_max_points_limit(qtbot, mocked_client):
"""Test that the buffer doesn't exceed max_points."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
# Add more points than max_points
mm._buffer = {"x": [1.0, 2.0, 3.0, 4.0], "y": [5.0, 6.0, 7.0, 8.0]}
@@ -219,7 +219,7 @@ def test_motor_map_limit_map(qtbot, mocked_client):
def test_motor_map_change_limits(qtbot, mocked_client):
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
# Original mocked limits are
# samx: [-10, 10]
@@ -229,8 +229,8 @@ def test_motor_map_change_limits(qtbot, mocked_client):
rect = mm._limit_map.boundingRect()
assert rect.width() == 20 # -10 to 10 inclusive
assert rect.height() == 10 # -5 to 5 inclusive
assert mm.config.x_motor.limits == [-10, 10]
assert mm.config.y_motor.limits == [-5, 5]
assert mm.config.device_x.limits == [-10, 10]
assert mm.config.device_y.limits == [-5, 5]
# Change the limits of the samx motor
mm.dev["samx"].limits = [-20, 20]
@@ -239,8 +239,8 @@ def test_motor_map_change_limits(qtbot, mocked_client):
qtbot.wait(200) # Allow time for the update to process
# Check that the limits map was updated
assert mm.config.x_motor.limits == [-20, 20]
assert mm.config.y_motor.limits == [-5, 5]
assert mm.config.device_x.limits == [-20, 20]
assert mm.config.device_y.limits == [-5, 5]
rect = mm._limit_map.boundingRect()
assert rect.width() == 40 # -20 to 20 inclusive
assert rect.height() == 10 # -5 to 5 inclusive -> same as before
@@ -276,13 +276,13 @@ def test_motor_map_toolbar_selection(qtbot, mocked_client):
motor_selection.widget.motor_x.setCurrentText("samx")
motor_selection.widget.motor_y.setCurrentText("samy")
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samy"
assert mm.config.device_x.device == "samx"
assert mm.config.device_y.device == "samy"
motor_selection.widget.motor_y.setCurrentText("samz")
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samz"
assert mm.config.device_x.device == "samx"
assert mm.config.device_y.device == "samz"
def test_motor_selection_set_motors_blocks_signals(qtbot, mocked_client):
@@ -306,19 +306,19 @@ def test_motor_properties_partial_then_complete_map(qtbot, mocked_client):
mm = create_widget(qtbot, MotorMap, client=mocked_client)
spy = QSignalSpy(mm.property_changed)
mm.x_motor = "samx"
mm.device_x = "samx"
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name is None
assert mm.config.device_x.device == "samx"
assert mm.config.device_y.device is None
assert mm._trace is None # map not triggered yet
assert spy.at(0) == ["x_motor", "samx"]
assert spy.at(0) == ["device_x", "samx"]
mm.y_motor = "samy"
mm.device_y = "samy"
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samy"
assert mm.config.device_x.device == "samx"
assert mm.config.device_y.device == "samy"
assert mm._trace is not None # map called once both valid
assert spy.at(1) == ["y_motor", "samy"]
assert spy.at(1) == ["device_y", "samy"]
assert len(mm._buffer["x"]) == 1
assert len(mm._buffer["y"]) == 1
@@ -331,9 +331,9 @@ def test_set_motor_name_emits_and_syncs_toolbar(qtbot, mocked_client):
spy = QSignalSpy(mm.property_changed)
mm._set_motor_name("x", "samx")
assert mm.config.x_motor.name == "samx"
assert mm.config.device_x.device == "samx"
assert motor_selection.motor_x.currentText() == "samx"
assert spy.at(0) == ["x_motor", "samx"]
assert spy.at(0) == ["device_x", "samx"]
# Calling with same name should be a no-op
initial_count = spy.count()
@@ -350,7 +350,7 @@ def test_motor_map_settings_dialog(qtbot, mocked_client):
assert action_ref().action.isVisible()
# set properties to be fetched by dialog
mm.map(x_name="samx", y_name="samy")
mm.map(device_x="samx", device_y="samy")
mm.precision = 2
mm.max_points = 1000
mm.scatter_size = 10

View File

@@ -37,7 +37,7 @@ def test_scatter_waveform_plot(qtbot, mocked_client):
assert curve is not None
assert isinstance(curve.config, ScatterCurveConfig)
assert curve.config.x_device == ScatterDeviceSignal(name="samx", entry="samx")
assert curve.config.device_x == ScatterDeviceSignal(device="samx", signal="samx")
assert curve.config.label == "bpm4i-bpm4i"
@@ -144,49 +144,49 @@ def test_device_safe_properties_get(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Initially devices should be empty
assert swf.x_device_name == ""
assert swf.x_device_entry == ""
assert swf.y_device_name == ""
assert swf.y_device_entry == ""
assert swf.z_device_name == ""
assert swf.z_device_entry == ""
assert swf.device_x == ""
assert swf.signal_x == ""
assert swf.device_y == ""
assert swf.signal_y == ""
assert swf.device_z == ""
assert swf.signal_z == ""
# Set devices via plot
swf.plot(x_name="samx", y_name="samy", z_name="bpm4i")
swf.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Check properties return device names and entries separately
assert swf.x_device_name == "samx"
assert swf.x_device_entry # Should have some entry
assert swf.y_device_name == "samy"
assert swf.y_device_entry # Should have some entry
assert swf.z_device_name == "bpm4i"
assert swf.z_device_entry # Should have some entry
assert swf.device_x == "samx"
assert swf.signal_x # Should have some entry
assert swf.device_y == "samy"
assert swf.signal_y # Should have some entry
assert swf.device_z == "bpm4i"
assert swf.signal_z # Should have some entry
def test_device_safe_properties_set_name(qtbot, mocked_client):
"""Test that device SafeProperty setters work for device names."""
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set x_device_name - should auto-validate entry
swf.x_device_name = "samx"
assert swf._main_curve.config.x_device is not None
assert swf._main_curve.config.x_device.name == "samx"
assert swf._main_curve.config.x_device.entry is not None # Entry should be validated
assert swf.x_device_name == "samx"
# Set device_x - should auto-validate entry
swf.device_x = "samx"
assert swf._main_curve.config.device_x is not None
assert swf._main_curve.config.device_x.device == "samx"
assert swf._main_curve.config.device_x.signal is not None # Entry should be validated
assert swf.device_x == "samx"
# Set y_device_name
swf.y_device_name = "samy"
assert swf._main_curve.config.y_device is not None
assert swf._main_curve.config.y_device.name == "samy"
assert swf._main_curve.config.y_device.entry is not None
assert swf.y_device_name == "samy"
# Set device_y
swf.device_y = "samy"
assert swf._main_curve.config.device_y is not None
assert swf._main_curve.config.device_y.device == "samy"
assert swf._main_curve.config.device_y.signal is not None
assert swf.device_y == "samy"
# Set z_device_name
swf.z_device_name = "bpm4i"
assert swf._main_curve.config.z_device is not None
assert swf._main_curve.config.z_device.name == "bpm4i"
assert swf._main_curve.config.z_device.entry is not None
assert swf.z_device_name == "bpm4i"
# Set device_z
swf.device_z = "bpm4i"
assert swf._main_curve.config.device_z is not None
assert swf._main_curve.config.device_z.device == "bpm4i"
assert swf._main_curve.config.device_z.signal is not None
assert swf.device_z == "bpm4i"
def test_device_safe_properties_set_entry(qtbot, mocked_client):
@@ -194,24 +194,24 @@ def test_device_safe_properties_set_entry(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set device name first - this auto-validates entry
swf.x_device_name = "samx"
initial_entry = swf.x_device_entry
swf.device_x = "samx"
initial_entry = swf.signal_x
assert initial_entry # Should have auto-validated entry
# Override with specific entry
swf.x_device_entry = "samx"
assert swf._main_curve.config.x_device.entry == "samx"
assert swf.x_device_entry == "samx"
swf.signal_x = "samx"
assert swf._main_curve.config.device_x.signal == "samx"
assert swf.signal_x == "samx"
# Same for y device
swf.y_device_name = "samy"
swf.y_device_entry = "samy_setpoint"
assert swf._main_curve.config.y_device.entry == "samy_setpoint"
swf.device_y = "samy"
swf.signal_y = "samy_setpoint"
assert swf._main_curve.config.device_y.signal == "samy_setpoint"
# Same for z device
swf.z_device_name = "bpm4i"
swf.z_device_entry = "bpm4i"
assert swf._main_curve.config.z_device.entry == "bpm4i"
swf.device_z = "bpm4i"
swf.signal_z = "bpm4i"
assert swf._main_curve.config.device_z.signal == "bpm4i"
def test_device_entry_cannot_be_set_without_name(qtbot, mocked_client):
@@ -219,10 +219,10 @@ def test_device_entry_cannot_be_set_without_name(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Try to set entry without device name
swf.x_device_entry = "some_entry"
swf.signal_x = "some_entry"
# Should not crash, entry should remain empty
assert swf.x_device_entry == ""
assert swf._main_curve.config.x_device is None
assert swf.signal_x == ""
assert swf._main_curve.config.device_x is None
def test_device_safe_properties_set_empty(qtbot, mocked_client):
@@ -230,13 +230,13 @@ def test_device_safe_properties_set_empty(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set device first
swf.x_device_name = "samx"
assert swf._main_curve.config.x_device is not None
swf.device_x = "samx"
assert swf._main_curve.config.device_x is not None
# Set to empty string - should clear the device
swf.x_device_name = ""
assert swf.x_device_name == ""
assert swf._main_curve.config.x_device is None
swf.device_x = ""
assert swf.device_x == ""
assert swf._main_curve.config.device_x is None
def test_device_safe_properties_auto_plot(qtbot, mocked_client):
@@ -244,14 +244,14 @@ def test_device_safe_properties_auto_plot(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set all three devices
swf.x_device_name = "samx"
swf.y_device_name = "samy"
swf.z_device_name = "bpm4i"
swf.device_x = "samx"
swf.device_y = "samy"
swf.device_z = "bpm4i"
# Check that plot was called (config should be updated)
assert swf._main_curve.config.x_device is not None
assert swf._main_curve.config.y_device is not None
assert swf._main_curve.config.z_device is not None
assert swf._main_curve.config.device_x is not None
assert swf._main_curve.config.device_y is not None
assert swf._main_curve.config.device_z is not None
def test_device_properties_update_labels(qtbot, mocked_client):
@@ -259,11 +259,11 @@ def test_device_properties_update_labels(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set x device - should update x label
swf.x_device_name = "samx"
swf.device_x = "samx"
assert swf.x_label == "samx"
# Set y device - should update y label
swf.y_device_name = "samy"
swf.device_y = "samy"
assert swf.y_label == "samy"
# Note: ScatterWaveform doesn't have a title like Heatmap does for z_device
@@ -274,39 +274,39 @@ def test_device_properties_partial_configuration(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set only x device
swf.x_device_name = "samx"
assert swf.x_device_name == "samx"
assert swf.y_device_name == ""
assert swf.z_device_name == ""
swf.device_x = "samx"
assert swf.device_x == "samx"
assert swf.device_y == ""
assert swf.device_z == ""
# Set only y device (x already set)
swf.y_device_name = "samy"
assert swf.x_device_name == "samx"
assert swf.y_device_name == "samy"
assert swf.z_device_name == ""
swf.device_y = "samy"
assert swf.device_x == "samx"
assert swf.device_y == "samy"
assert swf.device_z == ""
# Auto-plot should not trigger yet (z missing)
# But devices should be configured
assert swf._main_curve.config.x_device is not None
assert swf._main_curve.config.y_device is not None
assert swf._main_curve.config.device_x is not None
assert swf._main_curve.config.device_y is not None
def test_device_properties_in_user_access(qtbot, mocked_client):
"""Test that device properties are exposed in USER_ACCESS for RPC."""
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
assert "x_device_name" in ScatterWaveform.USER_ACCESS
assert "x_device_name.setter" in ScatterWaveform.USER_ACCESS
assert "x_device_entry" in ScatterWaveform.USER_ACCESS
assert "x_device_entry.setter" in ScatterWaveform.USER_ACCESS
assert "y_device_name" in ScatterWaveform.USER_ACCESS
assert "y_device_name.setter" in ScatterWaveform.USER_ACCESS
assert "y_device_entry" in ScatterWaveform.USER_ACCESS
assert "y_device_entry.setter" in ScatterWaveform.USER_ACCESS
assert "z_device_name" in ScatterWaveform.USER_ACCESS
assert "z_device_name.setter" in ScatterWaveform.USER_ACCESS
assert "z_device_entry" in ScatterWaveform.USER_ACCESS
assert "z_device_entry.setter" in ScatterWaveform.USER_ACCESS
assert "device_x" in ScatterWaveform.USER_ACCESS
assert "device_x.setter" in ScatterWaveform.USER_ACCESS
assert "signal_x" in ScatterWaveform.USER_ACCESS
assert "signal_x.setter" in ScatterWaveform.USER_ACCESS
assert "device_y" in ScatterWaveform.USER_ACCESS
assert "device_y.setter" in ScatterWaveform.USER_ACCESS
assert "signal_y" in ScatterWaveform.USER_ACCESS
assert "signal_y.setter" in ScatterWaveform.USER_ACCESS
assert "device_z" in ScatterWaveform.USER_ACCESS
assert "device_z.setter" in ScatterWaveform.USER_ACCESS
assert "signal_z" in ScatterWaveform.USER_ACCESS
assert "signal_z.setter" in ScatterWaveform.USER_ACCESS
def test_device_properties_validation(qtbot, mocked_client):
@@ -314,15 +314,15 @@ def test_device_properties_validation(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set device name - entry should be auto-validated
swf.x_device_name = "samx"
initial_entry = swf.x_device_entry
swf.device_x = "samx"
initial_entry = swf.signal_x
# The entry should be validated (will be "samx" in the mock)
assert initial_entry == "samx"
# Set a different entry - should also be validated
swf.x_device_entry = "samx" # Use same name as validated entry
assert swf.x_device_entry == "samx"
swf.signal_x = "samx" # Use same name as validated entry
assert swf.signal_x == "samx"
def test_device_properties_with_plot_method(qtbot, mocked_client):
@@ -330,17 +330,17 @@ def test_device_properties_with_plot_method(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Use plot method
swf.plot(x_name="samx", y_name="samy", z_name="bpm4i")
swf.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Properties should reflect the plotted devices
assert swf.x_device_name == "samx"
assert swf.y_device_name == "samy"
assert swf.z_device_name == "bpm4i"
assert swf.device_x == "samx"
assert swf.device_y == "samy"
assert swf.device_z == "bpm4i"
# Entries should be validated
assert swf.x_device_entry == "samx"
assert swf.y_device_entry == "samy"
assert swf.z_device_entry == "bpm4i"
assert swf.signal_x == "samx"
assert swf.signal_y == "samy"
assert swf.signal_z == "bpm4i"
def test_device_properties_overwrite_via_properties(qtbot, mocked_client):
@@ -348,16 +348,16 @@ def test_device_properties_overwrite_via_properties(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# First set via plot
swf.plot(x_name="samx", y_name="samy", z_name="bpm4i")
swf.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Overwrite x device via properties
swf.x_device_name = "samz"
assert swf.x_device_name == "samz"
assert swf._main_curve.config.x_device.name == "samz"
swf.device_x = "samz"
assert swf.device_x == "samz"
assert swf._main_curve.config.device_x.device == "samz"
# Overwrite y device entry
swf.y_device_entry = "samy"
assert swf.y_device_entry == "samy"
swf.signal_y = "samy"
assert swf.signal_y == "samy"
def test_device_properties_clearing_devices(qtbot, mocked_client):
@@ -365,18 +365,18 @@ def test_device_properties_clearing_devices(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set all devices
swf.x_device_name = "samx"
swf.y_device_name = "samy"
swf.z_device_name = "bpm4i"
swf.device_x = "samx"
swf.device_y = "samy"
swf.device_z = "bpm4i"
# Clear x device
swf.x_device_name = ""
assert swf.x_device_name == ""
assert swf._main_curve.config.x_device is None
swf.device_x = ""
assert swf.device_x == ""
assert swf._main_curve.config.device_x is None
# Y and Z should still be set
assert swf.y_device_name == "samy"
assert swf.z_device_name == "bpm4i"
assert swf.device_y == "samy"
assert swf.device_z == "bpm4i"
def test_device_properties_property_changed_signal(qtbot, mocked_client):
@@ -390,12 +390,12 @@ def test_device_properties_property_changed_signal(qtbot, mocked_client):
swf.property_changed.connect(mock_handler)
# Set device name
swf.x_device_name = "samx"
swf.device_x = "samx"
# Signal should have been emitted
assert mock_handler.called
# Check it was called with correct arguments
mock_handler.assert_any_call("x_device_name", "samx")
mock_handler.assert_any_call("device_x", "samx")
def test_device_entry_validation_with_invalid_device(qtbot, mocked_client):
@@ -403,7 +403,7 @@ def test_device_entry_validation_with_invalid_device(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Try to set invalid device name
swf.x_device_name = "nonexistent_device"
swf.device_x = "nonexistent_device"
# Should not crash, but device might not be set if validation fails
# The implementation silently fails, so we just check it doesn't crash
@@ -414,17 +414,17 @@ def test_device_properties_sequential_entry_changes(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Set device
swf.x_device_name = "samx"
swf.device_x = "samx"
# Change entry multiple times
swf.x_device_entry = "samx_velocity"
assert swf.x_device_entry == "samx_velocity"
swf.signal_x = "samx_velocity"
assert swf.signal_x == "samx_velocity"
swf.x_device_entry = "samx_setpoint"
assert swf.x_device_entry == "samx_setpoint"
swf.signal_x = "samx_setpoint"
assert swf.signal_x == "samx_setpoint"
swf.x_device_entry = "samx"
assert swf.x_device_entry == "samx"
swf.signal_x = "samx"
assert swf.signal_x == "samx"
def test_device_properties_with_none_values(qtbot, mocked_client):
@@ -432,15 +432,15 @@ def test_device_properties_with_none_values(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# Device name None should be treated as empty
swf.x_device_name = None
assert swf.x_device_name == ""
swf.device_x = None
assert swf.device_x == ""
# Set a device first
swf.y_device_name = "samy"
swf.device_y = "samy"
# Entry None should not change anything
swf.y_device_entry = None
assert swf.y_device_entry # Should still have validated entry
swf.signal_y = None
assert swf.signal_y # Should still have validated entry
################################################################################
@@ -457,9 +457,9 @@ def test_scatter_curve_settings_accept_changes(qtbot, mocked_client):
qtbot.addWidget(settings)
# Set up the widgets with test values
settings.ui.x_name.set_device("samx")
settings.ui.y_name.set_device("samy")
settings.ui.z_name.set_device("bpm4i")
settings.ui.device_x.set_device("samx")
settings.ui.device_y.set_device("samy")
settings.ui.device_z.set_device("bpm4i")
# Mock the plot method to verify it gets called with correct arguments
with patch.object(swf, "plot") as mock_plot:
@@ -472,9 +472,9 @@ def test_scatter_curve_settings_accept_changes(qtbot, mocked_client):
call_kwargs = mock_plot.call_args[1]
# Verify device names were extracted correctly
assert call_kwargs["x_name"] == "samx"
assert call_kwargs["y_name"] == "samy"
assert call_kwargs["z_name"] == "bpm4i"
assert call_kwargs["device_x"] == "samx"
assert call_kwargs["device_y"] == "samy"
assert call_kwargs["device_z"] == "bpm4i"
def test_scatter_curve_settings_accept_changes_with_entries(qtbot, mocked_client):
@@ -486,9 +486,9 @@ def test_scatter_curve_settings_accept_changes_with_entries(qtbot, mocked_client
qtbot.addWidget(settings)
# Set devices first to populate signal comboboxes
settings.ui.x_name.set_device("samx")
settings.ui.y_name.set_device("samy")
settings.ui.z_name.set_device("bpm4i")
settings.ui.device_x.set_device("samx")
settings.ui.device_y.set_device("samy")
settings.ui.device_z.set_device("bpm4i")
qtbot.wait(100) # Allow time for signals to populate
# Mock the plot method
@@ -499,9 +499,9 @@ def test_scatter_curve_settings_accept_changes_with_entries(qtbot, mocked_client
call_kwargs = mock_plot.call_args[1]
# Verify entries are extracted (will use get_signal_name())
assert "x_entry" in call_kwargs
assert "y_entry" in call_kwargs
assert "z_entry" in call_kwargs
assert "signal_x" in call_kwargs
assert "signal_y" in call_kwargs
assert "signal_z" in call_kwargs
def test_scatter_curve_settings_accept_changes_color_map(qtbot, mocked_client):
@@ -514,9 +514,9 @@ def test_scatter_curve_settings_accept_changes_color_map(qtbot, mocked_client):
qtbot.addWidget(settings)
# Set devices
settings.ui.x_name.set_device("samx")
settings.ui.y_name.set_device("samy")
settings.ui.z_name.set_device("bpm4i")
settings.ui.device_x.set_device("samx")
settings.ui.device_y.set_device("samy")
settings.ui.device_z.set_device("bpm4i")
# Get the current colormap
color_map = settings.ui.color_map.colormap
@@ -532,13 +532,13 @@ def test_scatter_curve_settings_fetch_all_properties(qtbot, mocked_client):
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
# First set up the scatter waveform with some data
swf.plot(x_name="samx", y_name="samy", z_name="bpm4i")
swf.plot(device_x="samx", device_y="samy", device_z="bpm4i")
# Create the settings widget - it should fetch properties automatically
settings = ScatterCurveSettings(parent=None, target_widget=swf, popup=True)
qtbot.addWidget(settings)
# Verify the settings widget has fetched the values
assert settings.ui.x_name.currentText() == "samx"
assert settings.ui.y_name.currentText() == "samy"
assert settings.ui.z_name.currentText() == "bpm4i"
assert settings.ui.device_x.currentText() == "samx"
assert settings.ui.device_y.currentText() == "samy"
assert settings.ui.device_z.currentText() == "bpm4i"

View File

@@ -107,8 +107,8 @@ def test_plot_single_arg_input_sync(qtbot, mocked_client):
assert c1.config.source == "device"
assert c2.config.source == "device"
assert c1.config.signal == DeviceSignal(name="bpm4i", entry="bpm4i", dap=None)
assert c2.config.signal == DeviceSignal(name="bpm3a", entry="bpm3a", dap=None)
assert c1.config.signal == DeviceSignal(device="bpm4i", signal="bpm4i", dap=None)
assert c2.config.signal == DeviceSignal(device="bpm3a", signal="bpm3a", dap=None)
# Check that the curve is added to the plot
assert len(wf.plot_item.curves) == 2
@@ -122,8 +122,8 @@ def test_plot_single_arg_input_async(qtbot, mocked_client):
assert c1.config.source == "device"
assert c2.config.source == "device"
assert c1.config.signal == DeviceSignal(name="eiger", entry="eiger", dap=None)
assert c2.config.signal == DeviceSignal(name="async_device", entry="async_device", dap=None)
assert c1.config.signal == DeviceSignal(device="eiger", signal="eiger", dap=None)
assert c2.config.signal == DeviceSignal(device="async_device", signal="async_device", dap=None)
# Check that the curve is added to the plot
assert len(wf.plot_item.curves) == 2
@@ -305,7 +305,7 @@ def test_curve_json_setter_ignores_custom(qtbot, mocked_client):
"label": "device_curve",
"color": "#ff0000",
"source": "device",
"signal": {"name": "bpm4i", "entry": "bpm4i", "dap": None},
"signal": {"device": "bpm4i", "signal": "bpm4i", "dap": None},
}
custom_curve_config = {
"widget_class": "Curve",
@@ -475,7 +475,7 @@ def test_add_dap_curve(qtbot, mocked_client_with_dap, monkeypatch):
dap_curve = wf.add_dap_curve(device_label="bpm4i-bpm4i", dap_name="GaussianModel")
assert dap_curve is not None
assert dap_curve.config.source == "dap"
assert dap_curve.config.signal.name == "bpm4i"
assert dap_curve.config.signal.device == "bpm4i"
assert dap_curve.config.signal.dap == "GaussianModel"
@@ -491,8 +491,8 @@ def test_add_dap_curve_custom_source(qtbot, mocked_client_with_dap):
dap_curve = wf.add_dap_curve(device_label=custom_curve.name(), dap_name="GaussianModel")
assert dap_curve.config.source == "dap"
assert dap_curve.config.parent_label == custom_curve.name()
assert dap_curve.config.signal.name == custom_curve.name()
assert dap_curve.config.signal.entry == "custom"
assert dap_curve.config.signal.device == custom_curve.name()
assert dap_curve.config.signal.signal == "custom"
assert dap_curve.config.signal.dap == "GaussianModel"
@@ -764,7 +764,7 @@ def test_curve_set_data_error_non_custom(qtbot, mocked_client):
Test that calling set_data on a non-custom (device) curve raises a ValueError.
"""
wf = create_widget(qtbot, Waveform, client=mocked_client)
# Create a device curve by providing y_name (which makes source 'device')
# Create a device curve by providing device_y (which makes source 'device')
# Assume that entry_validator returns a valid entry.
c = wf.plot(arg1="bpm4i", label="device_curve")
with pytest.raises(ValueError):
@@ -1136,20 +1136,20 @@ def test_update_with_scan_history_by_index(qtbot, mocked_client, scan_history_fa
assert len(wf.client.history._scan_ids) == 2, "Expected two history scans"
# Do history curve plotting
wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
wf.plot(y_name="bpm4i", scan_number=2)
wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id="hist1")
wf.plot(device_y="bpm4i", scan_number=2)
assert len(wf.plot_item.curves) == 2, "Expected two curves for history scans"
c1, c2 = wf.plot_item.curves
# First curve should be for hist1, second for hist2
assert c1.config.signal.name == "bpm4i"
assert c1.config.signal.entry == "bpm4i"
assert c1.config.signal.device == "bpm4i"
assert c1.config.signal.signal == "bpm4i"
assert c1.config.scan_id == "hist1"
assert c1.config.scan_number == 1
assert c1.name() == "bpm4i-bpm4i-scan-1"
assert c2.config.signal.name == "bpm4i"
assert c2.config.signal.entry == "bpm4i"
assert c2.config.signal.device == "bpm4i"
assert c2.config.signal.signal == "bpm4i"
assert c2.config.scan_id == "hist2"
assert c2.config.scan_number == 2
assert c2.name() == "bpm4i-bpm4i-scan-2"
@@ -1163,7 +1163,7 @@ def test_history_curve_x_modes_pre_plot(qtbot, mocked_client, scan_history_facto
wf = create_widget(qtbot, Waveform, client=mocked_client)
hist1, hist2 = inject_scan_history(wf, scan_history_factory, ("hist1", 1), ("hist2", 2))
wf.x_mode = mode
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id="hist1")
assert c.config.current_x_mode == mode
@@ -1174,7 +1174,7 @@ def test_history_curve_x_modes_post_plot(qtbot, mocked_client, scan_history_fact
"""
wf = create_widget(qtbot, Waveform, client=mocked_client)
hist1, hist2 = inject_scan_history(wf, scan_history_factory, ("hist1", 1), ("hist2", 2))
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="hist1")
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id="hist1")
# Change x_mode after plotting
wf.x_mode = mode
# Refresh history curves
@@ -1191,7 +1191,7 @@ def test_history_curve_incompatible_x_mode_hides_curve(qtbot, mocked_client, sca
# Inject history scan for this test
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_bad", 1))
# Plot history curve
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
# Curve should be hidden due to incompatible x_mode
assert not c.isVisible()
@@ -1212,7 +1212,7 @@ def test_fetch_history_data_no_stored_data_raises(
# Force get_history_scan_item to return our dummy
monkeypatch.setattr(wf, "get_history_scan_item", lambda scan_id, scan_index: dummy_scan)
# Attempt to plot history curve should be suppressed by SafeSlot and return None
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="dummy", scan_number=1)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id="dummy", scan_number=1)
assert c is None
assert len(wf.curves) == 0
@@ -1224,7 +1224,7 @@ def test_history_curve_device_missing_returns_none(qtbot, mocked_client, scan_hi
wf = create_widget(qtbot, Waveform, client=mocked_client)
wf.x_mode = "index"
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_dev_missing", 1))
c = wf.plot(y_name="non-existing", y_entry="non-existing", scan_id=history_msg.scan_id)
c = wf.plot(device_y="non-existing", signal_y="non-existing", scan_id=history_msg.scan_id)
assert c is None
@@ -1238,7 +1238,7 @@ def test_history_curve_custom_shape_mismatch_hides_curve(
wf.x_mode = "async_device"
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_custom_shape", 1))
# Force shape mismatch for x-data
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
assert c is not None
assert not c.isVisible()
@@ -1250,7 +1250,7 @@ def test_history_curve_index_mode_plots_curve(qtbot, mocked_client, scan_history
wf = create_widget(qtbot, Waveform, client=mocked_client)
wf.x_mode = "index"
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_index", 1))
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
assert c is not None
assert c.isVisible()
assert c.config.current_x_mode == "index"
@@ -1263,7 +1263,7 @@ def test_history_curve_timestamp_mode_plots_curve(qtbot, mocked_client, scan_his
wf = create_widget(qtbot, Waveform, client=mocked_client)
wf.x_mode = "timestamp"
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_time", 1))
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
assert c is not None
assert c.isVisible()
assert c.config.current_x_mode == "timestamp"
@@ -1279,7 +1279,7 @@ def test_history_curve_auto_valid_uses_first_report_device(
wf.x_mode = "auto"
[history_msg] = inject_scan_history(wf, scan_history_factory, ("hist_auto_valid", 1))
# Plot history curve
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
assert c is not None
assert c.isVisible()
# Should have fallen back to the first scan_report_device
@@ -1295,7 +1295,7 @@ def test_history_curve_file_not_found_returns_none(qtbot, mocked_client, scan_hi
# Inject a valid history message then corrupt its file_path
[history_msg] = inject_scan_history(wf, scan_history_factory, ("bad_file", 1))
history_msg.file_path = "/nonexistent/path.h5"
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id=history_msg.scan_id)
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id=history_msg.scan_id)
assert c is None
@@ -1306,5 +1306,5 @@ def test_history_curve_scan_not_found_returns_none(qtbot, mocked_client):
wf = create_widget(qtbot, Waveform, client=mocked_client)
wf.x_mode = "index"
# No history scans injected for this widget
c = wf.plot(y_name="bpm4i", y_entry="bpm4i", scan_id="unknown_scan")
c = wf.plot(device_y="bpm4i", signal_y="bpm4i", scan_id="unknown_scan")
assert c is None