mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
refactor(bec_figure): BECFigure removed
This commit is contained in:
@ -20,7 +20,6 @@ from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
@ -58,7 +57,7 @@ class BECWidgetsCLIServer:
|
||||
dispatcher: BECDispatcher = None,
|
||||
client=None,
|
||||
config=None,
|
||||
gui_class: Union[BECFigure, BECDockArea] = BECDockArea,
|
||||
gui_class: BECDockArea = BECDockArea,
|
||||
gui_class_id: str = "bec",
|
||||
) -> None:
|
||||
self.status = messages.BECStatus.BUSY
|
||||
@ -205,10 +204,7 @@ class SimpleFileLikeFromLogOutputFunc:
|
||||
|
||||
|
||||
def _start_server(
|
||||
gui_id: str,
|
||||
gui_class: Union[BECFigure, BECDockArea],
|
||||
gui_class_id: str = "bec",
|
||||
config: str | None = None,
|
||||
gui_id: str, gui_class: BECDockArea, gui_class_id: str = "bec", config: str | None = None
|
||||
):
|
||||
if config:
|
||||
try:
|
||||
@ -268,8 +264,6 @@ def main():
|
||||
|
||||
if args.gui_class == "BECDockArea":
|
||||
gui_class = BECDockArea
|
||||
elif args.gui_class == "BECFigure":
|
||||
gui_class = BECFigure
|
||||
else:
|
||||
print(
|
||||
"Please specify a valid gui_class to run. Use -h for help."
|
||||
|
@ -17,7 +17,6 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy as wh
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
from bec_widgets.widgets.plots_next_gen.image.image import Image
|
||||
@ -43,18 +42,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"np": np,
|
||||
"pg": pg,
|
||||
"wh": wh,
|
||||
"fig": self.figure,
|
||||
"dock": self.dock,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"w4": self.w4,
|
||||
"w5": self.w5,
|
||||
"w6": self.w6,
|
||||
"w7": self.w7,
|
||||
"w8": self.w8,
|
||||
"w9": self.w9,
|
||||
"w10": self.w10,
|
||||
"im": self.im,
|
||||
"mi": self.mi,
|
||||
"mm": self.mm,
|
||||
@ -89,12 +77,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
first_tab_layout.addWidget(self.dock)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
|
||||
second_tab = QWidget()
|
||||
second_tab_layout = QVBoxLayout(second_tab)
|
||||
self.figure = BECFigure(parent=self, gui_id="figure")
|
||||
second_tab_layout.addWidget(self.figure)
|
||||
tab_widget.addTab(second_tab, "BEC Figure")
|
||||
|
||||
third_tab = QWidget()
|
||||
third_tab_layout = QVBoxLayout(third_tab)
|
||||
self.lm = LayoutManagerWidget()
|
||||
@ -164,79 +146,16 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_waveform(self):
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve1")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve2")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve3")
|
||||
self.wf.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
self.wf.plot(y_name="bpm3a", y_entry="bpm3a", dap="GaussianModel")
|
||||
|
||||
def _init_figure(self):
|
||||
self.w1 = self.figure.plot(x_name="samx", y_name="bpm4i", row=0, col=0)
|
||||
self.w1.set(
|
||||
title="Standard Plot with sync device, custom labels - w1",
|
||||
x_label="Motor Position",
|
||||
y_label="Intensity (A.U.)",
|
||||
)
|
||||
self.w2 = self.figure.motor_map("samx", "samy", row=0, col=1)
|
||||
self.w3 = self.figure.image(
|
||||
"eiger", color_map="viridis", vrange=(0, 100), title="Eiger Image - w3", row=0, col=2
|
||||
)
|
||||
self.w4 = self.figure.plot(
|
||||
x_name="samx",
|
||||
y_name="samy",
|
||||
z_name="bpm4i",
|
||||
color_map_z="magma",
|
||||
new=True,
|
||||
title="2D scatter plot - w4",
|
||||
row=0,
|
||||
col=3,
|
||||
)
|
||||
self.w5 = self.figure.plot(
|
||||
y_name="bpm4i",
|
||||
new=True,
|
||||
title="Best Effort Plot - w5",
|
||||
dap="GaussianModel",
|
||||
row=1,
|
||||
col=0,
|
||||
)
|
||||
self.w6 = self.figure.plot(
|
||||
x_name="timestamp", y_name="bpm4i", new=True, title="Timestamp Plot - w6", row=1, col=1
|
||||
)
|
||||
self.w7 = self.figure.plot(
|
||||
x_name="index", y_name="bpm4i", new=True, title="Index Plot - w7", row=1, col=2
|
||||
)
|
||||
self.w8 = self.figure.plot(
|
||||
y_name="monitor_async", new=True, title="Async Plot - Best Effort - w8", row=2, col=0
|
||||
)
|
||||
self.w9 = self.figure.plot(
|
||||
x_name="timestamp",
|
||||
y_name="monitor_async",
|
||||
new=True,
|
||||
title="Async Plot - timestamp - w9",
|
||||
row=2,
|
||||
col=1,
|
||||
)
|
||||
self.w10 = self.figure.plot(
|
||||
x_name="index",
|
||||
y_name="monitor_async",
|
||||
new=True,
|
||||
title="Async Plot - index - w10",
|
||||
row=2,
|
||||
col=2,
|
||||
)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Override to handle things when main window is closed."""
|
||||
self.dock.cleanup()
|
||||
self.dock.close()
|
||||
self.figure.cleanup()
|
||||
self.figure.close()
|
||||
self.console.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
@ -1 +0,0 @@
|
||||
from .figure import BECFigure, FigureConfig
|
@ -1,789 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from typeguard import typechecked
|
||||
|
||||
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image import BECImageShow, ImageConfig
|
||||
from bec_widgets.widgets.containers.figure.plots.motor_map.motor_map import (
|
||||
BECMotorMap,
|
||||
MotorMapConfig,
|
||||
)
|
||||
from bec_widgets.widgets.containers.figure.plots.multi_waveform.multi_waveform import (
|
||||
BECMultiWaveform,
|
||||
BECMultiWaveformConfig,
|
||||
)
|
||||
from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform.waveform import (
|
||||
BECWaveform,
|
||||
Waveform1DConfig,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FigureConfig(ConnectionConfig):
|
||||
"""Configuration for BECFigure. Inheriting from ConnectionConfig widget_class and gui_id"""
|
||||
|
||||
theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
|
||||
num_cols: int = Field(1, description="The number of columns in the figure widget.")
|
||||
num_rows: int = Field(1, description="The number of rows in the figure widget.")
|
||||
widgets: dict[str, Waveform1DConfig | ImageConfig | MotorMapConfig | SubplotConfig] = Field(
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
@field_validator("widgets", mode="before")
|
||||
@classmethod
|
||||
def validate_widgets(cls, v):
|
||||
"""Validate the widgets configuration."""
|
||||
widget_class_map = {
|
||||
"BECWaveform": Waveform1DConfig,
|
||||
"BECImageShow": ImageConfig,
|
||||
"BECMotorMap": MotorMapConfig,
|
||||
}
|
||||
validated_widgets = {}
|
||||
for key, widget_config in v.items():
|
||||
if "widget_class" not in widget_config:
|
||||
raise ValueError(f"Widget config for {key} does not contain 'widget_class'.")
|
||||
widget_class = widget_config["widget_class"]
|
||||
if widget_class not in widget_class_map:
|
||||
raise ValueError(f"Unknown widget_class '{widget_class}' for widget '{key}'.")
|
||||
config_class = widget_class_map[widget_class]
|
||||
validated_widgets[key] = config_class(**widget_config)
|
||||
return validated_widgets
|
||||
|
||||
|
||||
class WidgetHandler:
|
||||
"""Factory for creating and configuring BEC widgets for BECFigure."""
|
||||
|
||||
def __init__(self):
|
||||
self.widget_factory = {
|
||||
"BECPlotBase": (BECPlotBase, SubplotConfig),
|
||||
"BECWaveform": (BECWaveform, Waveform1DConfig),
|
||||
"BECImageShow": (BECImageShow, ImageConfig),
|
||||
"BECMotorMap": (BECMotorMap, MotorMapConfig),
|
||||
"BECMultiWaveform": (BECMultiWaveform, BECMultiWaveformConfig),
|
||||
}
|
||||
|
||||
def create_widget(
|
||||
self, widget_type: str, parent_figure, parent_id: str, config: dict = None, **axis_kwargs
|
||||
) -> BECPlotBase:
|
||||
"""
|
||||
Create and configure a widget based on its type.
|
||||
|
||||
Args:
|
||||
widget_type (str): The type of the widget to create.
|
||||
widget_id (str): Unique identifier for the widget.
|
||||
parent_id (str): Identifier of the parent figure.
|
||||
config (dict, optional): Additional configuration for the widget.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECPlotBase: The created and configured widget instance.
|
||||
"""
|
||||
entry = self.widget_factory.get(widget_type)
|
||||
if not entry:
|
||||
raise ValueError(f"Unsupported widget type: {widget_type}")
|
||||
|
||||
widget_class, config_class = entry
|
||||
if config is not None and isinstance(config, config_class):
|
||||
config = config.model_dump()
|
||||
widget_config_dict = {
|
||||
"widget_class": widget_class.__name__,
|
||||
"parent_id": parent_id,
|
||||
**(config if config is not None else {}),
|
||||
}
|
||||
widget_config = config_class(**widget_config_dict)
|
||||
widget = widget_class(
|
||||
config=widget_config, parent_figure=parent_figure, client=parent_figure.client
|
||||
)
|
||||
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
|
||||
return widget
|
||||
|
||||
|
||||
class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
USER_ACCESS = [
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"_get_all_rpc",
|
||||
"axes",
|
||||
"widgets",
|
||||
"plot",
|
||||
"image",
|
||||
"motor_map",
|
||||
"remove",
|
||||
"change_layout",
|
||||
"change_theme",
|
||||
"export",
|
||||
"clear_all",
|
||||
"widget_list",
|
||||
]
|
||||
subplot_map = {
|
||||
"PlotBase": BECPlotBase,
|
||||
"BECWaveform": BECWaveform,
|
||||
"BECImageShow": BECImageShow,
|
||||
"BECMotorMap": BECMotorMap,
|
||||
"BECMultiWaveform": BECMultiWaveform,
|
||||
}
|
||||
widget_method_map = {
|
||||
"BECWaveform": "plot",
|
||||
"BECImageShow": "image",
|
||||
"BECMotorMap": "motor_map",
|
||||
"BECMultiWaveform": "multi_waveform",
|
||||
}
|
||||
|
||||
clean_signal = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
config: Optional[FigureConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = FigureConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = FigureConfig(**config)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
pg.GraphicsLayoutWidget.__init__(self, parent)
|
||||
|
||||
self.widget_handler = WidgetHandler()
|
||||
|
||||
# Widget container to reference widgets by 'widget_id'
|
||||
self._widgets = defaultdict(dict)
|
||||
|
||||
# Container to keep track of the grid
|
||||
self.grid = []
|
||||
# Create config and apply it
|
||||
self.apply_config(config)
|
||||
|
||||
def __getitem__(self, key: tuple | str):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
return self.axes(*key)
|
||||
if isinstance(key, str):
|
||||
widget = self._widgets.get(key)
|
||||
if widget is None:
|
||||
raise KeyError(f"No widget with ID {key}")
|
||||
return self._widgets.get(key)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
|
||||
)
|
||||
|
||||
def apply_config(self, config: dict | FigureConfig): # ,generate_new_id: bool = False):
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = FigureConfig(**config)
|
||||
except ValidationError as e:
|
||||
logger.error(f"Error in applying config: {e}")
|
||||
return
|
||||
self.config = config
|
||||
|
||||
# widget_config has to be reset for not have each widget config twice when added to the figure
|
||||
widget_configs = list(self.config.widgets.values())
|
||||
self.config.widgets = {}
|
||||
for widget_config in widget_configs:
|
||||
getattr(self, self.widget_method_map[widget_config.widget_class])(
|
||||
config=widget_config.model_dump(), row=widget_config.row, col=widget_config.col
|
||||
)
|
||||
|
||||
@property
|
||||
def widget_list(self) -> list[BECPlotBase]:
|
||||
"""
|
||||
Access all widget in BECFigure as a list
|
||||
Returns:
|
||||
list[BECPlotBase]: List of all widgets in the figure.
|
||||
"""
|
||||
axes = [value for value in self._widgets.values() if isinstance(value, BECPlotBase)]
|
||||
return axes
|
||||
|
||||
@widget_list.setter
|
||||
def widget_list(self, value: list[BECPlotBase]):
|
||||
"""
|
||||
Access all widget in BECFigure as a list
|
||||
Returns:
|
||||
list[BECPlotBase]: List of all widgets in the figure.
|
||||
"""
|
||||
self._axes = value
|
||||
|
||||
@property
|
||||
def widgets(self) -> dict:
|
||||
"""
|
||||
All widgets within the figure with gui ids as keys.
|
||||
Returns:
|
||||
dict: All widgets within the figure.
|
||||
"""
|
||||
return self._widgets
|
||||
|
||||
@widgets.setter
|
||||
def widgets(self, value: dict):
|
||||
"""
|
||||
All widgets within the figure with gui ids as keys.
|
||||
Returns:
|
||||
dict: All widgets within the figure.
|
||||
"""
|
||||
self._widgets = value
|
||||
|
||||
def export(self):
|
||||
"""Export the plot widget."""
|
||||
try:
|
||||
plot_item = self.widget_list[0]
|
||||
except Exception as exc:
|
||||
raise ValueError("No plot widget available to export.") from exc
|
||||
|
||||
scene = plot_item.scene()
|
||||
scene.contextMenuItem = plot_item
|
||||
scene.showExportDialog()
|
||||
|
||||
@typechecked
|
||||
def plot(
|
||||
self,
|
||||
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,
|
||||
z_name: str | None = None,
|
||||
x_entry: str | None = None,
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
dap: str | None = None,
|
||||
config: dict | None = None, # TODO make logic more transparent
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
|
||||
|
||||
Args:
|
||||
arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-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.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
dap(str): The DAP model to use for the curve.
|
||||
config(dict): Recreates the whole BECWaveform widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECWaveform: The waveform plot widget.
|
||||
"""
|
||||
waveform = self.subplot_factory(
|
||||
widget_type="BECWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if config is not None:
|
||||
return waveform
|
||||
|
||||
if arg1 is not None or y_name is not None or (y is not None and x is not None):
|
||||
waveform.plot(
|
||||
arg1=arg1,
|
||||
y=y,
|
||||
x=x,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
dap=dap,
|
||||
)
|
||||
return waveform
|
||||
|
||||
def _init_image(
|
||||
self,
|
||||
image,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Configure the image based on the provided parameters.
|
||||
|
||||
Args:
|
||||
image (BECImageShow): The image to configure.
|
||||
monitor (str): The name of the monitor to display.
|
||||
color_bar (Literal["simple","full"]): The type of color bar to display.
|
||||
color_map (str): The color map to use for the image.
|
||||
data (np.ndarray): Custom data to display.
|
||||
"""
|
||||
if monitor is not None and data is None:
|
||||
image.image(
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_map=color_map,
|
||||
vrange=vrange,
|
||||
color_bar=color_bar,
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is None and monitor is None:
|
||||
# Setting appearance
|
||||
if vrange is not None:
|
||||
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
|
||||
if color_map is not None:
|
||||
image.set_color_map(color_map)
|
||||
else:
|
||||
raise ValueError("Invalid input. Provide either monitor name or custom data.")
|
||||
return image
|
||||
|
||||
def image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
config: dict | None = None,
|
||||
**axis_kwargs,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Add an image to the figure. Always access the first image widget in the figure.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
vrange(tuple[float, float]): The range of values to display.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
|
||||
image = self.subplot_factory(
|
||||
widget_type="BECImageShow", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if config is not None:
|
||||
return image
|
||||
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def motor_map(
|
||||
self,
|
||||
motor_x: str = None,
|
||||
motor_y: str = None,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
config: dict | None = None,
|
||||
**axis_kwargs,
|
||||
) -> BECMotorMap:
|
||||
"""
|
||||
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
||||
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
motor_map = self.subplot_factory(
|
||||
widget_type="BECMotorMap", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if config is not None:
|
||||
return motor_map
|
||||
|
||||
if motor_x is not None and motor_y is not None:
|
||||
motor_map.change_motors(motor_x, motor_y)
|
||||
|
||||
return motor_map
|
||||
|
||||
def multi_waveform(
|
||||
self,
|
||||
monitor: str = None,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
config: dict | None = None,
|
||||
**axis_kwargs,
|
||||
):
|
||||
multi_waveform = self.subplot_factory(
|
||||
widget_type="BECMultiWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if config is not None:
|
||||
return multi_waveform
|
||||
multi_waveform.set_monitor(monitor)
|
||||
return multi_waveform
|
||||
|
||||
def subplot_factory(
|
||||
self,
|
||||
widget_type: Literal[
|
||||
"BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
|
||||
] = "BECPlotBase",
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
new: bool = False,
|
||||
**axis_kwargs,
|
||||
) -> BECPlotBase:
|
||||
# Case 1 - config provided, new plot, possible to define coordinates
|
||||
if config is not None:
|
||||
widget_cls = config["widget_class"]
|
||||
if widget_cls != widget_type:
|
||||
raise ValueError(
|
||||
f"Widget type '{widget_type}' does not match the provided configuration ({widget_cls})."
|
||||
)
|
||||
widget = self.add_widget(
|
||||
widget_type=widget_type, config=config, row=row, col=col, **axis_kwargs
|
||||
)
|
||||
return widget
|
||||
|
||||
# Case 2 - find first plot or create first plot if no plot available, no config provided, no coordinates
|
||||
if new is False and (row is None or col is None):
|
||||
widget = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, self.subplot_map[widget_type], can_fail=True
|
||||
)
|
||||
if widget is not None:
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
else:
|
||||
widget = self.add_widget(widget_type=widget_type, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
# Case 3 - modifying existing plot wit coordinates provided
|
||||
if new is False and (row is not None and col is not None):
|
||||
try:
|
||||
widget = self.axes(row, col)
|
||||
except ValueError:
|
||||
widget = None
|
||||
if widget is not None:
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
else:
|
||||
widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
# Case 4 - no previous plot or new plot, no config provided, possible to define coordinates
|
||||
widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget_type: Literal[
|
||||
"BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap", "BECMultiWaveform"
|
||||
] = "BECPlotBase",
|
||||
widget_id: str = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECPlotBase:
|
||||
"""
|
||||
Add a widget to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
widget_type(Literal["PlotBase","Waveform1D"]): The type of the widget to add.
|
||||
widget_id(str): The unique identifier of the widget. If not provided, a unique ID will be generated.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
if not widget_id:
|
||||
widget_id = str(uuid.uuid4())
|
||||
if widget_id in self._widgets:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
|
||||
|
||||
# Check if position is occupied
|
||||
if row is not None and col is not None:
|
||||
if self.getItem(row, col):
|
||||
raise ValueError(f"Position at row {row} and column {col} is already occupied.")
|
||||
else:
|
||||
row, col = self._find_next_empty_position()
|
||||
|
||||
widget = self.widget_handler.create_widget(
|
||||
widget_type=widget_type,
|
||||
parent_figure=self,
|
||||
parent_id=self.gui_id,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
widget_id = widget.gui_id
|
||||
|
||||
widget.config.row = row
|
||||
widget.config.col = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.addItem(widget, row=row, col=col)
|
||||
|
||||
# Update num_cols and num_rows based on the added widget
|
||||
self.config.num_rows = max(self.config.num_rows, row + 1)
|
||||
self.config.num_cols = max(self.config.num_cols, col + 1)
|
||||
|
||||
# Saving config for future referencing
|
||||
|
||||
self.config.widgets[widget_id] = widget.config
|
||||
self._widgets[widget_id] = widget
|
||||
|
||||
# Reflect the grid coordinates
|
||||
self._change_grid(widget_id, row, col)
|
||||
|
||||
return widget
|
||||
|
||||
def remove(
|
||||
self,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
widget_id: str = None,
|
||||
coordinates: tuple[int, int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Remove a widget from the figure. Can be removed by its unique identifier or by its coordinates.
|
||||
|
||||
Args:
|
||||
row(int): The row coordinate of the widget to remove.
|
||||
col(int): The column coordinate of the widget to remove.
|
||||
widget_id(str): The unique identifier of the widget to remove.
|
||||
coordinates(tuple[int, int], optional): The coordinates of the widget to remove.
|
||||
"""
|
||||
if widget_id:
|
||||
self._remove_by_id(widget_id)
|
||||
elif row is not None and col is not None:
|
||||
self._remove_by_coordinates(row, col)
|
||||
elif coordinates:
|
||||
self._remove_by_coordinates(*coordinates)
|
||||
else:
|
||||
raise ValueError("Must provide either widget_id or coordinates for removal.")
|
||||
|
||||
def change_theme(self, theme: Literal["dark", "light"]) -> None:
|
||||
"""
|
||||
Change the theme of the figure widget.
|
||||
|
||||
Args:
|
||||
theme(Literal["dark","light"]): The theme to set for the figure widget.
|
||||
"""
|
||||
self.config.theme = theme
|
||||
apply_theme(theme)
|
||||
for plot in self.widget_list:
|
||||
plot.set_x_label(plot.plot_item.getAxis("bottom").label.toPlainText())
|
||||
plot.set_y_label(plot.plot_item.getAxis("left").label.toPlainText())
|
||||
if plot.plot_item.titleLabel.text:
|
||||
plot.set_title(plot.plot_item.titleLabel.text)
|
||||
plot.set_legend_label_size()
|
||||
|
||||
def _remove_by_coordinates(self, row: int, col: int) -> None:
|
||||
"""
|
||||
Remove a widget from the figure by its coordinates.
|
||||
|
||||
Args:
|
||||
row(int): The row coordinate of the widget to remove.
|
||||
col(int): The column coordinate of the widget to remove.
|
||||
"""
|
||||
widget = self.axes(row, col)
|
||||
if widget:
|
||||
widget_id = widget.config.gui_id
|
||||
if widget_id in self._widgets:
|
||||
self._remove_by_id(widget_id)
|
||||
|
||||
def _remove_by_id(self, widget_id: str) -> None:
|
||||
"""
|
||||
Remove a widget from the figure by its unique identifier.
|
||||
|
||||
Args:
|
||||
widget_id(str): The unique identifier of the widget to remove.
|
||||
"""
|
||||
if widget_id in self._widgets:
|
||||
widget = self._widgets.pop(widget_id)
|
||||
widget.cleanup_pyqtgraph()
|
||||
widget.cleanup()
|
||||
self.removeItem(widget)
|
||||
self.grid[widget.config.row][widget.config.col] = None
|
||||
self._reindex_grid()
|
||||
if widget_id in self.config.widgets:
|
||||
self.config.widgets.pop(widget_id)
|
||||
widget.deleteLater()
|
||||
else:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
|
||||
|
||||
def axes(self, row: int, col: int) -> BECPlotBase:
|
||||
"""
|
||||
Get widget by its coordinates in the figure.
|
||||
|
||||
Args:
|
||||
row(int): the row coordinate
|
||||
col(int): the column coordinate
|
||||
|
||||
Returns:
|
||||
BECPlotBase: the widget at the given coordinates
|
||||
"""
|
||||
widget = self.getItem(row, col)
|
||||
if widget is None:
|
||||
raise ValueError(f"No widget at coordinates ({row}, {col})")
|
||||
return widget
|
||||
|
||||
def _find_next_empty_position(self):
|
||||
"""Find the next empty position (new row) in the figure."""
|
||||
row, col = 0, 0
|
||||
while self.getItem(row, col):
|
||||
row += 1
|
||||
return row, col
|
||||
|
||||
def _change_grid(self, widget_id: str, row: int, col: int):
|
||||
"""
|
||||
Change the grid to reflect the new position of the widget.
|
||||
|
||||
Args:
|
||||
widget_id(str): The unique identifier of the widget.
|
||||
row(int): The new row coordinate of the widget in the figure.
|
||||
col(int): The new column coordinate of the widget in the figure.
|
||||
"""
|
||||
while len(self.grid) <= row:
|
||||
self.grid.append([])
|
||||
row = self.grid[row]
|
||||
while len(row) <= col:
|
||||
row.append(None)
|
||||
row[col] = widget_id
|
||||
|
||||
def _reindex_grid(self):
|
||||
"""Reindex the grid to remove empty rows and columns."""
|
||||
new_grid = []
|
||||
for row in self.grid:
|
||||
new_row = [widget for widget in row if widget is not None]
|
||||
if new_row:
|
||||
new_grid.append(new_row)
|
||||
#
|
||||
# Update the config of each object to reflect its new position
|
||||
for row_idx, row in enumerate(new_grid):
|
||||
for col_idx, widget in enumerate(row):
|
||||
self._widgets[widget].config.row, self._widgets[widget].config.col = (
|
||||
row_idx,
|
||||
col_idx,
|
||||
)
|
||||
|
||||
self.grid = new_grid
|
||||
self._replot_layout()
|
||||
|
||||
def _replot_layout(self):
|
||||
"""Replot the layout based on the current grid configuration."""
|
||||
self.clear()
|
||||
for row_idx, row in enumerate(self.grid):
|
||||
for col_idx, widget in enumerate(row):
|
||||
self.addItem(self._widgets[widget], row=row_idx, col=col_idx)
|
||||
|
||||
def change_layout(self, max_columns=None, max_rows=None):
|
||||
"""
|
||||
Reshuffle the layout of the figure to adjust to a new number of max_columns or max_rows.
|
||||
If both max_columns and max_rows are provided, max_rows is ignored.
|
||||
|
||||
Args:
|
||||
max_columns (Optional[int]): The new maximum number of columns in the figure.
|
||||
max_rows (Optional[int]): The new maximum number of rows in the figure.
|
||||
"""
|
||||
# Calculate total number of widgets
|
||||
total_widgets = len(self._widgets)
|
||||
|
||||
if max_columns:
|
||||
# Calculate the required number of rows based on max_columns
|
||||
required_rows = (total_widgets + max_columns - 1) // max_columns
|
||||
new_grid = [[None for _ in range(max_columns)] for _ in range(required_rows)]
|
||||
elif max_rows:
|
||||
# Calculate the required number of columns based on max_rows
|
||||
required_columns = (total_widgets + max_rows - 1) // max_rows
|
||||
new_grid = [[None for _ in range(required_columns)] for _ in range(max_rows)]
|
||||
else:
|
||||
# If neither max_columns nor max_rows is specified, just return without changing the layout
|
||||
return
|
||||
|
||||
# Populate the new grid with widgets' IDs
|
||||
current_idx = 0
|
||||
for widget_id in self._widgets:
|
||||
row = current_idx // len(new_grid[0])
|
||||
col = current_idx % len(new_grid[0])
|
||||
new_grid[row][col] = widget_id
|
||||
current_idx += 1
|
||||
|
||||
self.config.num_rows = row
|
||||
self.config.num_cols = col
|
||||
|
||||
# Update widgets' positions and replot them according to the new grid
|
||||
self.grid = new_grid
|
||||
self._reindex_grid() # This method should be updated to handle reshuffling correctly
|
||||
self._replot_layout() # Assumes this method re-adds widgets to the layout based on self.grid
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear all widgets from the figure and reset to default state"""
|
||||
for widget in list(self._widgets.values()):
|
||||
widget.remove()
|
||||
self._widgets.clear()
|
||||
self.grid = []
|
||||
theme = self.config.theme
|
||||
self.config = FigureConfig(
|
||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||
)
|
||||
|
||||
def cleanup_pyqtgraph_all_widgets(self):
|
||||
"""Clean up the pyqtgraph widget."""
|
||||
for widget in self.widget_list:
|
||||
widget.cleanup_pyqtgraph()
|
||||
|
||||
def cleanup(self):
|
||||
"""Close the figure widget."""
|
||||
self.cleanup_pyqtgraph_all_widgets()
|
@ -1,91 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtWidgets import QVBoxLayout
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class AxisSettings(SettingWidget):
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "axis_settings.ui"), self)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
|
||||
# Hardcoded values for best appearance
|
||||
self.setMinimumHeight(280)
|
||||
self.setMaximumHeight(280)
|
||||
self.resize(380, 280)
|
||||
|
||||
@Slot(dict)
|
||||
def display_current_settings(self, axis_config: dict):
|
||||
|
||||
if axis_config == {}:
|
||||
return
|
||||
|
||||
# Top Box
|
||||
WidgetIO.set_value(self.ui.plot_title, axis_config["title"])
|
||||
self.ui.switch_outer_axes.checked = axis_config["outer_axes"]
|
||||
|
||||
# X Axis Box
|
||||
WidgetIO.set_value(self.ui.x_label, axis_config["x_label"])
|
||||
WidgetIO.set_value(self.ui.x_scale, axis_config["x_scale"])
|
||||
WidgetIO.set_value(self.ui.x_grid, axis_config["x_grid"])
|
||||
if axis_config["x_lim"] is not None:
|
||||
WidgetIO.check_and_adjust_limits(self.ui.x_min, axis_config["x_lim"][0])
|
||||
WidgetIO.check_and_adjust_limits(self.ui.x_max, axis_config["x_lim"][1])
|
||||
WidgetIO.set_value(self.ui.x_min, axis_config["x_lim"][0])
|
||||
WidgetIO.set_value(self.ui.x_max, axis_config["x_lim"][1])
|
||||
if axis_config["x_lim"] is None:
|
||||
x_range = self.target_widget.fig.widget_list[0].plot_item.viewRange()[0]
|
||||
WidgetIO.set_value(self.ui.x_min, x_range[0])
|
||||
WidgetIO.set_value(self.ui.x_max, x_range[1])
|
||||
|
||||
# Y Axis Box
|
||||
WidgetIO.set_value(self.ui.y_label, axis_config["y_label"])
|
||||
WidgetIO.set_value(self.ui.y_scale, axis_config["y_scale"])
|
||||
WidgetIO.set_value(self.ui.y_grid, axis_config["y_grid"])
|
||||
if axis_config["y_lim"] is not None:
|
||||
WidgetIO.check_and_adjust_limits(self.ui.y_min, axis_config["y_lim"][0])
|
||||
WidgetIO.check_and_adjust_limits(self.ui.y_max, axis_config["y_lim"][1])
|
||||
WidgetIO.set_value(self.ui.y_min, axis_config["y_lim"][0])
|
||||
WidgetIO.set_value(self.ui.y_max, axis_config["y_lim"][1])
|
||||
if axis_config["y_lim"] is None:
|
||||
y_range = self.target_widget.fig.widget_list[0].plot_item.viewRange()[1]
|
||||
WidgetIO.set_value(self.ui.y_min, y_range[0])
|
||||
WidgetIO.set_value(self.ui.y_max, y_range[1])
|
||||
|
||||
@Slot()
|
||||
def accept_changes(self):
|
||||
title = WidgetIO.get_value(self.ui.plot_title)
|
||||
outer_axes = self.ui.switch_outer_axes.checked
|
||||
|
||||
# X Axis
|
||||
x_label = WidgetIO.get_value(self.ui.x_label)
|
||||
x_scale = self.ui.x_scale.currentText()
|
||||
x_grid = WidgetIO.get_value(self.ui.x_grid)
|
||||
x_lim = (WidgetIO.get_value(self.ui.x_min), WidgetIO.get_value(self.ui.x_max))
|
||||
|
||||
# Y Axis
|
||||
y_label = WidgetIO.get_value(self.ui.y_label)
|
||||
y_scale = self.ui.y_scale.currentText()
|
||||
y_grid = WidgetIO.get_value(self.ui.y_grid)
|
||||
y_lim = (WidgetIO.get_value(self.ui.y_min), WidgetIO.get_value(self.ui.y_max))
|
||||
|
||||
self.target_widget.set(
|
||||
title=title,
|
||||
x_label=x_label,
|
||||
x_scale=x_scale,
|
||||
x_lim=x_lim,
|
||||
y_label=y_label,
|
||||
y_scale=y_scale,
|
||||
y_lim=y_lim,
|
||||
)
|
||||
self.target_widget.set_grid(x_grid, y_grid)
|
||||
self.target_widget.set_outer_axes(outer_axes)
|
@ -1,256 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>427</width>
|
||||
<height>270</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>278</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="plot_title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="y_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="y_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="x_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="x_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="ToggleSwitch" name="switch_outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -1,773 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError
|
||||
from qtpy.QtCore import QThread, Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
# from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image_item import (
|
||||
BECImageItem,
|
||||
ImageItemConfig,
|
||||
)
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image_processor import (
|
||||
ImageProcessor,
|
||||
ImageStats,
|
||||
ProcessorWorker,
|
||||
)
|
||||
from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ImageConfig(SubplotConfig):
|
||||
images: dict[str, ImageItemConfig] = Field(
|
||||
{},
|
||||
description="The configuration of the images. The key is the name of the image (source).",
|
||||
)
|
||||
|
||||
|
||||
# TODO old version will be deprecated
|
||||
class BECImageShow(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"add_image_by_config",
|
||||
"image",
|
||||
"add_custom_image",
|
||||
"set_vrange",
|
||||
"set_color_map",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_monitor",
|
||||
"set_processing",
|
||||
"set_image_properties",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
"images",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[ImageConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
single_image: bool = True,
|
||||
):
|
||||
if config is None:
|
||||
config = ImageConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.single_image = single_image
|
||||
self.image_type = "device_monitor_2d"
|
||||
self.scan_id = None
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
self._images = defaultdict(dict)
|
||||
self.apply_config(self.config)
|
||||
self.processor = ImageProcessor()
|
||||
self.use_threading = False # TODO WILL be moved to the init method and to figure method
|
||||
|
||||
def _create_thread_worker(self, device: str, image: np.ndarray):
|
||||
thread = QThread()
|
||||
worker = ProcessorWorker(self.processor)
|
||||
worker.moveToThread(thread)
|
||||
|
||||
# Connect signals and slots
|
||||
thread.started.connect(lambda: worker.process_image(device, image))
|
||||
worker.processed.connect(self.update_image)
|
||||
worker.stats.connect(self.update_vrange)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.finished.connect(thread.wait)
|
||||
worker.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
|
||||
thread.start()
|
||||
|
||||
def find_image_by_monitor(self, item_id: str) -> BECImageItem:
|
||||
"""
|
||||
Find the image item by its gui_id.
|
||||
|
||||
Args:
|
||||
item_id(str): The gui_id of the widget.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The widget with the given gui_id.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for key, value in images.items():
|
||||
if key == item_id and isinstance(value, BECImageItem):
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
result = self.find_image_by_monitor(item_id)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
def apply_config(self, config: dict | SubplotConfig):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
|
||||
Args:
|
||||
config(dict|SubplotConfig): Configuration settings.
|
||||
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = ImageConfig(**config)
|
||||
except ValidationError as e:
|
||||
logger.error(f"Validation error when applying config to BECImageShow: {e}")
|
||||
return
|
||||
self.config = config
|
||||
self.plot_item.clear()
|
||||
|
||||
self.apply_axis_config()
|
||||
self._images = defaultdict(dict)
|
||||
|
||||
for image_id, image_config in config.images.items():
|
||||
self.add_image_by_config(image_config)
|
||||
|
||||
def change_gui_id(self, new_gui_id: str):
|
||||
"""
|
||||
Change the GUI ID of the image widget and update the parent_id in all associated curves.
|
||||
|
||||
Args:
|
||||
new_gui_id (str): The new GUI ID to be set for the image widget.
|
||||
"""
|
||||
self.gui_id = new_gui_id
|
||||
self.config.gui_id = new_gui_id
|
||||
|
||||
for source, images in self._images.items():
|
||||
for id, image_item in images.items():
|
||||
image_item.config.parent_id = new_gui_id
|
||||
|
||||
def add_image_by_config(self, config: ImageItemConfig | dict) -> BECImageItem:
|
||||
"""
|
||||
Add an image to the widget by configuration.
|
||||
|
||||
Args:
|
||||
config(ImageItemConfig|dict): The configuration of the image.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The image object.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = ImageItemConfig(**config)
|
||||
config.parent_id = self.gui_id
|
||||
name = config.monitor if config.monitor is not None else config.gui_id
|
||||
image = self._add_image_object(source=config.source, name=name, config=config)
|
||||
return image
|
||||
|
||||
def get_image_config(self, image_id, dict_output: bool = True) -> ImageItemConfig | dict:
|
||||
"""
|
||||
Get the configuration of the image.
|
||||
|
||||
Args:
|
||||
image_id(str): The ID of the image.
|
||||
dict_output(bool): Whether to return the configuration as a dictionary. Defaults to True.
|
||||
|
||||
Returns:
|
||||
ImageItemConfig|dict: The configuration of the image.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for id, image in images.items():
|
||||
if id == image_id:
|
||||
if dict_output:
|
||||
return image.config.dict()
|
||||
else:
|
||||
return image.config # TODO check if this works
|
||||
|
||||
@property
|
||||
def images(self) -> list[BECImageItem]:
|
||||
"""
|
||||
Get the list of images.
|
||||
Returns:
|
||||
list[BECImageItem]: The list of images.
|
||||
"""
|
||||
images = []
|
||||
for source, images_dict in self._images.items():
|
||||
for id, image in images_dict.items():
|
||||
images.append(image)
|
||||
return images
|
||||
|
||||
@images.setter
|
||||
def images(self, value: dict[str, dict[str, BECImageItem]]):
|
||||
"""
|
||||
Set the images from a dictionary.
|
||||
|
||||
Args:
|
||||
value (dict[str, dict[str, BECImageItem]]): The images to set, organized by source and id.
|
||||
"""
|
||||
self._images = value
|
||||
|
||||
def get_image_dict(self) -> dict[str, dict[str, BECImageItem]]:
|
||||
"""
|
||||
Get all images.
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, BECImageItem]]: The dictionary of images.
|
||||
"""
|
||||
return self._images
|
||||
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
# post_processing: Optional[PostProcessingConfig] = None,
|
||||
**kwargs,
|
||||
) -> BECImageItem:
|
||||
"""
|
||||
Add an image to the figure. Always access the first image widget in the figure.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
monitor_type(Literal["1d","2d"]): The type of monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
vrange(tuple[float, float]): The range of values to display.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The image item.
|
||||
"""
|
||||
if monitor_type == "1d":
|
||||
image_source = "device_monitor_1d"
|
||||
self.image_type = "device_monitor_1d"
|
||||
elif monitor_type == "2d":
|
||||
image_source = "device_monitor_2d"
|
||||
self.image_type = "device_monitor_2d"
|
||||
|
||||
image_exits = self._check_image_id(monitor, self._images)
|
||||
if image_exits:
|
||||
# raise ValueError(
|
||||
# f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
|
||||
# )
|
||||
return
|
||||
|
||||
# monitor = self.entry_validator.validate_monitor(monitor)
|
||||
|
||||
image_config = ImageItemConfig(
|
||||
widget_class="BECImageItem",
|
||||
parent_id=self.gui_id,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
opacity=opacity,
|
||||
vrange=vrange,
|
||||
source=image_source,
|
||||
monitor=monitor,
|
||||
# post_processing=post_processing,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(source=image_source, name=monitor, config=image_config)
|
||||
return image
|
||||
|
||||
def add_custom_image(
|
||||
self,
|
||||
name: str,
|
||||
data: Optional[np.ndarray] = None,
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
# post_processing: Optional[PostProcessingConfig] = None,
|
||||
**kwargs,
|
||||
):
|
||||
image_source = "custom"
|
||||
|
||||
image_exits = self._check_image_id(name, self._images)
|
||||
if image_exits:
|
||||
raise ValueError(f"Monitor with ID '{name}' already exists in widget '{self.gui_id}'.")
|
||||
|
||||
image_config = ImageItemConfig(
|
||||
widget_class="BECImageItem",
|
||||
parent_id=self.gui_id,
|
||||
monitor=name,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
opacity=opacity,
|
||||
vrange=vrange,
|
||||
# post_processing=post_processing,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(
|
||||
source=image_source, name=name, config=image_config, data=data
|
||||
)
|
||||
return image
|
||||
|
||||
def apply_setting_to_images(
|
||||
self, setting_method_name: str, args: list, kwargs: dict, image_id: str = None
|
||||
):
|
||||
"""
|
||||
Apply a setting to all images or a specific image by its ID.
|
||||
|
||||
Args:
|
||||
setting_method_name (str): The name of the method to apply (e.g., 'set_color_map').
|
||||
args (list): Positional arguments for the setting method.
|
||||
kwargs (dict): Keyword arguments for the setting method.
|
||||
image_id (str, optional): The ID of the specific image to apply the setting to. If None, applies to all images.
|
||||
"""
|
||||
if image_id:
|
||||
image = self.find_image_by_monitor(image_id)
|
||||
if image:
|
||||
getattr(image, setting_method_name)(*args, **kwargs)
|
||||
else:
|
||||
for source, images in self._images.items():
|
||||
for _, image in images.items():
|
||||
getattr(image, setting_method_name)(*args, **kwargs)
|
||||
self.refresh_image()
|
||||
|
||||
def set_vrange(self, vmin: float, vmax: float, name: str = None):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
If name is not specified, then set vrange for all images.
|
||||
|
||||
Args:
|
||||
vmin(float): Minimum value of the color bar.
|
||||
vmax(float): Maximum value of the color bar.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_vrange", args=[vmin, vmax], kwargs={}, image_id=name)
|
||||
|
||||
def set_color_map(self, cmap: str, name: str = None):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
If name is not specified, then set color map for all images.
|
||||
|
||||
Args:
|
||||
cmap(str): The color map of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_color_map", args=[cmap], kwargs={}, image_id=name)
|
||||
|
||||
def set_autorange(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the autoscale of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to autoscale the color bar.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"], name: str = None):
|
||||
"""
|
||||
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
|
||||
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
|
||||
|
||||
Args:
|
||||
mode(str): The autoscale mode of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange_mode", args=[mode], kwargs={}, image_id=name)
|
||||
|
||||
def set_monitor(self, monitor: str, name: str = None):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
If name is not specified, then set monitor for all images.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_monitor", args=[monitor], kwargs={}, image_id=name)
|
||||
|
||||
def set_processing(self, name: str = None, **kwargs):
|
||||
"""
|
||||
Set the post processing of the image.
|
||||
If name is not specified, then set post processing for all images.
|
||||
|
||||
Args:
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
Possible properties:
|
||||
- fft: bool
|
||||
- log: bool
|
||||
- rot: int
|
||||
- transpose: bool
|
||||
"""
|
||||
self.apply_setting_to_images("set", args=[], kwargs=kwargs, image_id=name)
|
||||
|
||||
def set_image_properties(self, name: str = None, **kwargs):
|
||||
"""
|
||||
Set the properties of the image.
|
||||
|
||||
Args:
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
Possible properties:
|
||||
- downsample: bool
|
||||
- color_map: str
|
||||
- monitor: str
|
||||
- opacity: float
|
||||
- vrange: tuple[int,int]
|
||||
- fft: bool
|
||||
- log: bool
|
||||
- rot: int
|
||||
- transpose: bool
|
||||
"""
|
||||
self.apply_setting_to_images("set", args=[], kwargs=kwargs, image_id=name)
|
||||
|
||||
def set_fft(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the FFT of the image.
|
||||
If name is not specified, then set FFT for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform FFT on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_fft", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_log(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the log of the image.
|
||||
If name is not specified, then set log for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform log on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_log", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_rotation(self, deg_90: int = 0, name: str = None):
|
||||
"""
|
||||
Set the rotation of the image.
|
||||
If name is not specified, then set rotation for all images.
|
||||
|
||||
Args:
|
||||
deg_90(int): The rotation angle of the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_rotation", args=[deg_90], kwargs={}, image_id=name)
|
||||
|
||||
def set_transpose(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the transpose of the image.
|
||||
If name is not specified, then set transpose for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to transpose the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_transpose", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def toggle_threading(self, use_threading: bool):
|
||||
"""
|
||||
Toggle threading for the widgets postprocessing and updating.
|
||||
|
||||
Args:
|
||||
use_threading(bool): Whether to use threading.
|
||||
"""
|
||||
self.use_threading = use_threading
|
||||
if self.use_threading is False and self.thread.isRunning():
|
||||
self.cleanup()
|
||||
|
||||
def process_image(self, device: str, image: BECImageItem, data: np.ndarray):
|
||||
"""
|
||||
Process the image data.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device - image_id of image.
|
||||
image(np.ndarray): The image data to be processed.
|
||||
data(np.ndarray): The image data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed image data.
|
||||
"""
|
||||
processing_config = image.config.processing
|
||||
self.processor.set_config(processing_config)
|
||||
if self.use_threading:
|
||||
self._create_thread_worker(device, data)
|
||||
else:
|
||||
data = self.processor.process_image(data)
|
||||
self.update_image(device, data)
|
||||
self.update_vrange(device, self.processor.config.stats)
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_image_update(self, msg: dict, metadata: dict):
|
||||
"""
|
||||
Update the image of the device monitor from bec.
|
||||
|
||||
Args:
|
||||
msg(dict): The message from bec.
|
||||
metadata(dict): The metadata of the message.
|
||||
"""
|
||||
data = msg["data"]
|
||||
device = msg["device"]
|
||||
if self.image_type == "device_monitor_1d":
|
||||
image = self._images["device_monitor_1d"][device]
|
||||
current_scan_id = metadata.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
if current_scan_id != self.scan_id:
|
||||
self.reset()
|
||||
self.scan_id = current_scan_id
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
image_buffer = self.adjust_image_buffer(image, data)
|
||||
image.raw_data = image_buffer
|
||||
self.process_image(device, image, image_buffer)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
image = self._images["device_monitor_2d"][device]
|
||||
image.raw_data = data
|
||||
self.process_image(device, image, data)
|
||||
|
||||
def adjust_image_buffer(self, image: BECImageItem, new_data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Adjusts the image buffer to accommodate the new data, ensuring that all rows have the same length.
|
||||
|
||||
Args:
|
||||
image: The image object (used to store buffer list and max_len).
|
||||
new_data (np.ndarray): The new incoming 1D waveform data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The updated image buffer with adjusted shapes.
|
||||
"""
|
||||
new_len = new_data.shape[0]
|
||||
if not hasattr(image, "image_buffer_list"):
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
|
||||
if new_len > image.max_len:
|
||||
image.max_len = new_len
|
||||
for i in range(len(image.image_buffer_list)):
|
||||
wf = image.image_buffer_list[i]
|
||||
pad_width = image.max_len - wf.shape[0]
|
||||
if pad_width > 0:
|
||||
image.image_buffer_list[i] = np.pad(
|
||||
wf, (0, pad_width), mode="constant", constant_values=0
|
||||
)
|
||||
image.image_buffer_list.append(new_data)
|
||||
else:
|
||||
pad_width = image.max_len - new_len
|
||||
if pad_width > 0:
|
||||
new_data = np.pad(new_data, (0, pad_width), mode="constant", constant_values=0)
|
||||
image.image_buffer_list.append(new_data)
|
||||
|
||||
image_buffer = np.array(image.image_buffer_list)
|
||||
return image_buffer
|
||||
|
||||
@Slot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
"""
|
||||
Update the image of the device monitor.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device.
|
||||
data(np.ndarray): The data to be updated.
|
||||
"""
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
@Slot(str, ImageStats)
|
||||
def update_vrange(self, device: str, stats: ImageStats):
|
||||
"""
|
||||
Update the scaling of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The statistics of the image.
|
||||
"""
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
if image_to_update.config.autorange:
|
||||
image_to_update.auto_update_vrange(stats)
|
||||
|
||||
def refresh_image(self):
|
||||
"""
|
||||
Refresh the image.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for image_id, image in images.items():
|
||||
data = image.raw_data
|
||||
self.process_image(image_id, image, data)
|
||||
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
"""
|
||||
image_item = self.find_image_by_monitor(monitor)
|
||||
try:
|
||||
previous_monitor = image_item.config.monitor
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
if previous_monitor and image_item.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(previous_monitor)
|
||||
)
|
||||
image_item.connected = False
|
||||
if monitor and image_item.connected is False:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
if self.image_type == "device_monitor_1d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
image_item.set_monitor(monitor)
|
||||
image_item.connected = True
|
||||
|
||||
def _add_image_object(
|
||||
self, source: str, name: str, config: ImageItemConfig, data=None
|
||||
) -> BECImageItem:
|
||||
config.parent_id = self.gui_id
|
||||
if self.single_image is True and len(self.images) > 0:
|
||||
self.remove_image(0)
|
||||
image = BECImageItem(config=config, parent_image=self)
|
||||
self.plot_item.addItem(image)
|
||||
self._images[source][name] = image
|
||||
self._connect_device_monitor(config.monitor)
|
||||
self.config.images[name] = config
|
||||
if data is not None:
|
||||
image.setImage(data)
|
||||
return image
|
||||
|
||||
def _check_image_id(self, val: Any, dict_to_check: dict) -> bool:
|
||||
"""
|
||||
Check if val is in the values of the dict_to_check or in the values of the nested dictionaries.
|
||||
|
||||
Args:
|
||||
val(Any): Value to check.
|
||||
dict_to_check(dict): Dictionary to check.
|
||||
|
||||
Returns:
|
||||
bool: True if val is in the values of the dict_to_check or in the values of the nested dictionaries, False otherwise.
|
||||
"""
|
||||
if val in dict_to_check.keys():
|
||||
return True
|
||||
for key in dict_to_check:
|
||||
if isinstance(dict_to_check[key], dict):
|
||||
if self._check_image_id(val, dict_to_check[key]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_image(self, *identifiers):
|
||||
"""
|
||||
Remove an image from the plot widget.
|
||||
|
||||
Args:
|
||||
*identifiers: Identifier of the image to be removed. Can be either an integer (index) or a string (image_id).
|
||||
"""
|
||||
for identifier in identifiers:
|
||||
if isinstance(identifier, int):
|
||||
self._remove_image_by_order(identifier)
|
||||
elif isinstance(identifier, str):
|
||||
self._remove_image_by_id(identifier)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Each identifier must be either an integer (index) or a string (image_id)."
|
||||
)
|
||||
|
||||
def _remove_image_by_id(self, image_id):
|
||||
for source, images in self._images.items():
|
||||
if image_id in images:
|
||||
self._disconnect_monitor(image_id)
|
||||
image = images.pop(image_id)
|
||||
self.removeItem(image.color_bar)
|
||||
self.plot_item.removeItem(image)
|
||||
del self.config.images[image_id]
|
||||
if image in self.images:
|
||||
self.images.remove(image)
|
||||
return
|
||||
raise KeyError(f"Image with ID '{image_id}' not found.")
|
||||
|
||||
def _remove_image_by_order(self, N):
|
||||
"""
|
||||
Remove an image by its order from the plot widget.
|
||||
|
||||
Args:
|
||||
N(int): Order of the image to be removed.
|
||||
"""
|
||||
if N < len(self.images):
|
||||
image = self.images[N]
|
||||
image_id = image.config.monitor
|
||||
self._disconnect_monitor(image_id)
|
||||
self.removeItem(image.color_bar)
|
||||
self.plot_item.removeItem(image)
|
||||
del self.config.images[image_id]
|
||||
for source, images in self._images.items():
|
||||
if image_id in images:
|
||||
del images[image_id]
|
||||
break
|
||||
else:
|
||||
raise IndexError(f"Image order {N} out of range.")
|
||||
|
||||
def _disconnect_monitor(self, image_id):
|
||||
"""
|
||||
Disconnect the monitor from the device.
|
||||
|
||||
Args:
|
||||
image_id(str): The ID of the monitor.
|
||||
"""
|
||||
image = self.find_image_by_monitor(image_id)
|
||||
if image:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(image.config.monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(image.config.monitor)
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the widget.
|
||||
"""
|
||||
for monitor in self._images[self.image_type]:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
self.images.clear()
|
||||
|
||||
def cleanup_pyqtgraph(self):
|
||||
"""Cleanup pyqtgraph items."""
|
||||
super().cleanup_pyqtgraph()
|
||||
item = self.plot_item
|
||||
if not item.items:
|
||||
return
|
||||
cbar = item.items[0].color_bar
|
||||
cbar.vb.menu.close()
|
||||
cbar.vb.menu.deleteLater()
|
||||
cbar.gradient.menu.close()
|
||||
cbar.gradient.menu.deleteLater()
|
||||
cbar.gradient.colorDialog.close()
|
||||
cbar.gradient.colorDialog.deleteLater()
|
@ -1,338 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image_processor import (
|
||||
ImageStats,
|
||||
ProcessingConfig,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image import BECImageShow
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ImageItemConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the image.")
|
||||
monitor: Optional[str] = Field(None, description="The name of the monitor.")
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
color_map: Optional[str] = Field("magma", description="The color map of the image.")
|
||||
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
|
||||
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
|
||||
vrange: Optional[tuple[float | int, float | int]] = Field(
|
||||
None, description="The range of the color bar. If None, the range is automatically set."
|
||||
)
|
||||
color_bar: Optional[Literal["simple", "full"]] = Field(
|
||||
"simple", description="The type of the color bar."
|
||||
)
|
||||
autorange: Optional[bool] = Field(True, description="Whether to autorange the color bar.")
|
||||
autorange_mode: Optional[Literal["max", "mean"]] = Field(
|
||||
"mean", description="Whether to use the mean of the image for autoscaling."
|
||||
)
|
||||
processing: ProcessingConfig = Field(
|
||||
default_factory=ProcessingConfig, description="The post processing of the image."
|
||||
)
|
||||
|
||||
|
||||
# TODO old version will be deprecated
|
||||
class BECImageItem(BECConnector, pg.ImageItem):
|
||||
USER_ACCESS = [
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"set_opacity",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_color_map",
|
||||
"set_auto_downsample",
|
||||
"set_monitor",
|
||||
"set_vrange",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[ImageItemConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_image: Optional[BECImageShow] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = ImageItemConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
||||
pg.ImageItem.__init__(self)
|
||||
|
||||
self.parent_image = parent_image
|
||||
self.colorbar_bar = None
|
||||
self._raw_data = None
|
||||
|
||||
self._add_color_bar(
|
||||
self.config.color_bar, self.config.vrange
|
||||
) # TODO can also support None to not have any colorbar
|
||||
self.apply_config()
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
self.connected = False
|
||||
|
||||
@property
|
||||
def raw_data(self) -> np.ndarray:
|
||||
return self._raw_data
|
||||
|
||||
@raw_data.setter
|
||||
def raw_data(self, data: np.ndarray):
|
||||
self._raw_data = data
|
||||
|
||||
def apply_config(self):
|
||||
"""
|
||||
Apply current configuration.
|
||||
"""
|
||||
self.set_color_map(self.config.color_map)
|
||||
self.set_auto_downsample(self.config.downsample)
|
||||
if self.config.vrange is not None:
|
||||
self.set_vrange(vrange=self.config.vrange)
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the image.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- downsample
|
||||
- color_map
|
||||
- monitor
|
||||
- opacity
|
||||
- vrange
|
||||
- fft
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
- autorange_mode
|
||||
"""
|
||||
method_map = {
|
||||
"downsample": self.set_auto_downsample,
|
||||
"color_map": self.set_color_map,
|
||||
"monitor": self.set_monitor,
|
||||
"opacity": self.set_opacity,
|
||||
"vrange": self.set_vrange,
|
||||
"fft": self.set_fft,
|
||||
"log": self.set_log,
|
||||
"rot": self.set_rotation,
|
||||
"transpose": self.set_transpose,
|
||||
"autorange_mode": self.set_autorange_mode,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
logger.warning(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_fft(self, enable: bool = False):
|
||||
"""
|
||||
Set the FFT of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform FFT on the monitor data.
|
||||
"""
|
||||
self.config.processing.fft = enable
|
||||
|
||||
def set_log(self, enable: bool = False):
|
||||
"""
|
||||
Set the log of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform log on the monitor data.
|
||||
"""
|
||||
self.config.processing.log = enable
|
||||
if enable and self.color_bar and self.config.color_bar == "full":
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_rotation(self, deg_90: int = 0):
|
||||
"""
|
||||
Set the rotation of the image.
|
||||
|
||||
Args:
|
||||
deg_90(int): The rotation angle of the monitor data before displaying.
|
||||
"""
|
||||
self.config.processing.rotation = deg_90
|
||||
|
||||
def set_transpose(self, enable: bool = False):
|
||||
"""
|
||||
Set the transpose of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to transpose the image.
|
||||
"""
|
||||
self.config.processing.transpose = enable
|
||||
|
||||
def set_opacity(self, opacity: float = 1.0):
|
||||
"""
|
||||
Set the opacity of the image.
|
||||
|
||||
Args:
|
||||
opacity(float): The opacity of the image.
|
||||
"""
|
||||
self.setOpacity(opacity)
|
||||
self.config.opacity = opacity
|
||||
|
||||
def set_autorange(self, autorange: bool = False):
|
||||
"""
|
||||
Set the autorange of the color bar.
|
||||
|
||||
Args:
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
self.config.autorange = autorange
|
||||
if self.color_bar and autorange:
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"] = "mean"):
|
||||
"""
|
||||
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
|
||||
|
||||
Args:
|
||||
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
|
||||
"""
|
||||
self.config.autorange_mode = mode
|
||||
|
||||
def set_color_map(self, cmap: str = "magma"):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
|
||||
Args:
|
||||
cmap(str): The color map of the image.
|
||||
"""
|
||||
self.setColorMap(cmap)
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setColorMap(cmap)
|
||||
elif self.config.color_bar == "full":
|
||||
self.color_bar.gradient.loadPreset(cmap)
|
||||
self.config.color_map = cmap
|
||||
|
||||
def set_auto_downsample(self, auto: bool = True):
|
||||
"""
|
||||
Set the auto downsample of the image.
|
||||
|
||||
Args:
|
||||
auto(bool): Whether to downsample the image.
|
||||
"""
|
||||
self.setAutoDownsample(auto)
|
||||
self.config.downsample = auto
|
||||
|
||||
def set_monitor(self, monitor: str):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
"""
|
||||
self.config.monitor = monitor
|
||||
|
||||
def auto_update_vrange(self, stats: ImageStats) -> None:
|
||||
"""Auto update of the vrange base on the stats of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The stats of the image.
|
||||
"""
|
||||
fumble_factor = 2
|
||||
if self.config.autorange_mode == "mean":
|
||||
vmin = max(stats.mean - fumble_factor * stats.std, 0)
|
||||
vmax = stats.mean + fumble_factor * stats.std
|
||||
self.set_vrange(vmin, vmax, change_autorange=False)
|
||||
return
|
||||
if self.config.autorange_mode == "max":
|
||||
self.set_vrange(max(stats.minimum, 0), stats.maximum, change_autorange=False)
|
||||
return
|
||||
|
||||
def set_vrange(
|
||||
self,
|
||||
vmin: float = None,
|
||||
vmax: float = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
change_autorange: bool = True,
|
||||
):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
|
||||
Args:
|
||||
vmin(float): Minimum value of the color bar.
|
||||
vmax(float): Maximum value of the color bar.
|
||||
"""
|
||||
if vrange is not None:
|
||||
vmin, vmax = vrange
|
||||
self.setLevels([vmin, vmax])
|
||||
self.config.vrange = (vmin, vmax)
|
||||
if change_autorange:
|
||||
self.config.autorange = False
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setLevels(low=vmin, high=vmax)
|
||||
elif self.config.color_bar == "full":
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||
|
||||
def get_data(self) -> np.ndarray:
|
||||
"""
|
||||
Get the data of the image.
|
||||
Returns:
|
||||
np.ndarray: The data of the image.
|
||||
"""
|
||||
return self.image
|
||||
|
||||
def _add_color_bar(
|
||||
self, color_bar_style: str = "simple", vrange: Optional[tuple[int, int]] = None
|
||||
):
|
||||
"""
|
||||
Add color bar to the layout.
|
||||
|
||||
Args:
|
||||
style(Literal["simple,full"]): The style of the color bar.
|
||||
vrange(tuple[int,int]): The range of the color bar.
|
||||
"""
|
||||
if color_bar_style == "simple":
|
||||
self.color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
|
||||
if vrange is not None:
|
||||
self.color_bar.setLevels(low=vrange[0], high=vrange[1])
|
||||
self.color_bar.setImageItem(self)
|
||||
self.parent_image.addItem(self.color_bar, row=1, col=1)
|
||||
self.config.color_bar = "simple"
|
||||
elif color_bar_style == "full":
|
||||
# Setting histogram
|
||||
self.color_bar = pg.HistogramLUTItem()
|
||||
self.color_bar.setImageItem(self)
|
||||
self.color_bar.gradient.loadPreset(self.config.color_map)
|
||||
if vrange is not None:
|
||||
self.color_bar.setLevels(min=vrange[0], max=vrange[1])
|
||||
self.color_bar.setHistogramRange(
|
||||
vrange[0] - 0.1 * vrange[0], vrange[1] + 0.1 * vrange[1]
|
||||
)
|
||||
|
||||
# Adding histogram to the layout
|
||||
self.parent_image.addItem(self.color_bar, row=1, col=1)
|
||||
|
||||
# save settings
|
||||
self.config.color_bar = "full"
|
||||
else:
|
||||
raise ValueError("style should be 'simple' or 'full'")
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
self.parent_image.remove_image(self.config.monitor)
|
||||
self.rpc_register.remove_rpc(self)
|
@ -1,185 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
# TODO will be deleted
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageStats:
|
||||
"""Container to store stats of an image."""
|
||||
|
||||
maximum: float
|
||||
minimum: float
|
||||
mean: float
|
||||
std: float
|
||||
|
||||
|
||||
class ProcessingConfig(BaseModel):
|
||||
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
|
||||
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
|
||||
center_of_mass: Optional[bool] = Field(
|
||||
False, description="Whether to calculate the center of mass of the monitor data."
|
||||
)
|
||||
transpose: Optional[bool] = Field(
|
||||
False, description="Whether to transpose the monitor data before displaying."
|
||||
)
|
||||
rotation: Optional[int] = Field(
|
||||
None, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
stats: ImageStats = Field(
|
||||
ImageStats(maximum=0, minimum=0, mean=0, std=0),
|
||||
description="The statistics of the image data.",
|
||||
)
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
"""
|
||||
Class for processing the image data.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ProcessingConfig = None):
|
||||
if config is None:
|
||||
config = ProcessingConfig()
|
||||
self.config = config
|
||||
|
||||
def set_config(self, config: ProcessingConfig):
|
||||
"""
|
||||
Set the configuration of the processor.
|
||||
|
||||
Args:
|
||||
config(ProcessingConfig): The configuration of the processor.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def FFT(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Perform FFT on the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.abs(np.fft.fftshift(np.fft.fft2(data)))
|
||||
|
||||
def rotation(self, data: np.ndarray, rotate_90: int) -> np.ndarray:
|
||||
"""
|
||||
Rotate the data by 90 degrees n times.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
rotate_90(int): The number of 90 degree rotations.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.rot90(data, k=rotate_90, axes=(0, 1))
|
||||
|
||||
def transpose(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Transpose the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.transpose(data)
|
||||
|
||||
def log(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Perform log on the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
# TODO this is not final solution -> data should stay as int16
|
||||
data = data.astype(np.float32)
|
||||
offset = 1e-6
|
||||
data_offset = data + offset
|
||||
return np.log10(data_offset)
|
||||
|
||||
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
||||
# return np.unravel_index(np.argmax(data), data.shape)
|
||||
|
||||
def update_image_stats(self, data: np.ndarray) -> None:
|
||||
"""Get the statistics of the image data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The image data.
|
||||
|
||||
"""
|
||||
self.config.stats.maximum = np.max(data)
|
||||
self.config.stats.minimum = np.min(data)
|
||||
self.config.stats.mean = np.mean(data)
|
||||
self.config.stats.std = np.std(data)
|
||||
|
||||
def process_image(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Process the data according to the configuration.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
if self.config.fft:
|
||||
data = self.FFT(data)
|
||||
if self.config.rotation is not None:
|
||||
data = self.rotation(data, self.config.rotation)
|
||||
if self.config.transpose:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
data = self.log(data)
|
||||
self.update_image_stats(data)
|
||||
return data
|
||||
|
||||
|
||||
class ProcessorWorker(QObject):
|
||||
"""
|
||||
Worker for processing the image data.
|
||||
"""
|
||||
|
||||
processed = Signal(str, np.ndarray)
|
||||
stats = Signal(str, ImageStats)
|
||||
stopRequested = Signal()
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, processor):
|
||||
super().__init__()
|
||||
self.processor = processor
|
||||
self._isRunning = False
|
||||
self.stopRequested.connect(self.stop)
|
||||
|
||||
@Slot(str, np.ndarray)
|
||||
def process_image(self, device: str, image: np.ndarray):
|
||||
"""
|
||||
Process the image data.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device.
|
||||
image(np.ndarray): The image data.
|
||||
"""
|
||||
self._isRunning = True
|
||||
processed_image = self.processor.process_image(image)
|
||||
self._isRunning = False
|
||||
if not self._isRunning:
|
||||
self.processed.emit(device, processed_image)
|
||||
self.stats.emit(self.processor.config.stats)
|
||||
self.finished.emit()
|
||||
|
||||
def stop(self):
|
||||
self._isRunning = False
|
@ -1,526 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform.waveform import Signal, SignalData
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MotorMapConfig(SubplotConfig):
|
||||
signals: Optional[Signal] = Field(None, description="Signals of the motor map")
|
||||
color: Optional[str | tuple] = Field(
|
||||
(255, 255, 255, 255), description="The color of the last point of current position."
|
||||
)
|
||||
scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
|
||||
max_points: Optional[int] = Field(5000, description="Maximum number of points to display.")
|
||||
num_dim_points: Optional[int] = Field(
|
||||
100,
|
||||
description="Number of points to dim before the color remains same for older recorded position.",
|
||||
)
|
||||
precision: Optional[int] = Field(2, description="Decimal precision of the motor position.")
|
||||
background_value: Optional[int] = Field(
|
||||
25, description="Background value of the motor map. Has to be between 0 and 255."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
|
||||
@field_validator("background_value")
|
||||
def validate_background_value(cls, value):
|
||||
if not 0 <= value <= 255:
|
||||
raise PydanticCustomError(
|
||||
"wrong_value", f"'{value}' hs to be between 0 and 255.", {"wrong_value": value}
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class BECMotorMap(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"change_motors",
|
||||
"set_max_points",
|
||||
"set_precision",
|
||||
"set_num_dim_points",
|
||||
"set_background_value",
|
||||
"set_scatter_size",
|
||||
"get_data",
|
||||
"export",
|
||||
"remove",
|
||||
"reset_history",
|
||||
]
|
||||
|
||||
# QT Signals
|
||||
update_signal = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[MotorMapConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
# connect update signal to update plot
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||
)
|
||||
self.apply_config(self.config)
|
||||
|
||||
def apply_config(self, config: dict | MotorMapConfig):
|
||||
"""
|
||||
Apply the config to the motor map.
|
||||
|
||||
Args:
|
||||
config(dict|MotorMapConfig): Config to be applied.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = MotorMapConfig(**config)
|
||||
except ValidationError as e:
|
||||
logger.error(f"Error in applying config: {e}")
|
||||
return
|
||||
|
||||
self.config = config
|
||||
self.plot_item.clear()
|
||||
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
self.database_buffer = {"x": [], "y": []}
|
||||
self.plot_components = defaultdict(dict) # container for plot components
|
||||
|
||||
self.apply_axis_config()
|
||||
|
||||
if self.config.signals is not None:
|
||||
self.change_motors(
|
||||
motor_x=self.config.signals.x.name,
|
||||
motor_y=self.config.signals.y.name,
|
||||
motor_x_entry=self.config.signals.x.entry,
|
||||
motor_y_entry=self.config.signals.y.entry,
|
||||
)
|
||||
|
||||
@Slot(str, str, str, str, bool)
|
||||
def change_motors(
|
||||
self,
|
||||
motor_x: str,
|
||||
motor_y: str,
|
||||
motor_x_entry: str = None,
|
||||
motor_y_entry: str = None,
|
||||
validate_bec: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Change the active motors for the plot.
|
||||
|
||||
Args:
|
||||
motor_x(str): Motor name for the X axis.
|
||||
motor_y(str): Motor name for the Y axis.
|
||||
motor_x_entry(str): Motor entry for the X axis.
|
||||
motor_y_entry(str): Motor entry for the Y axis.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
self.plot_item.clear()
|
||||
|
||||
motor_x_entry, motor_y_entry = self._validate_signal_entries(
|
||||
motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
|
||||
)
|
||||
|
||||
motor_x_limit = self._get_motor_limit(motor_x)
|
||||
motor_y_limit = self._get_motor_limit(motor_y)
|
||||
|
||||
signal = Signal(
|
||||
source="device_readback",
|
||||
x=SignalData(name=motor_x, entry=motor_x_entry, limits=motor_x_limit),
|
||||
y=SignalData(name=motor_y, entry=motor_y_entry, limits=motor_y_limit),
|
||||
)
|
||||
self.config.signals = signal
|
||||
|
||||
# reconnect the signals
|
||||
self._connect_motor_to_slots()
|
||||
|
||||
self.database_buffer = {"x": [], "y": []}
|
||||
|
||||
# Redraw the motor map
|
||||
self._make_motor_map()
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
data = {"x": self.database_buffer["x"], "y": self.database_buffer["y"]}
|
||||
return data
|
||||
|
||||
def reset_history(self):
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
self.database_buffer["x"] = [self.database_buffer["x"][-1]]
|
||||
self.database_buffer["y"] = [self.database_buffer["y"][-1]]
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_color(self, color: str | tuple):
|
||||
"""
|
||||
Set color of the motor trace.
|
||||
|
||||
Args:
|
||||
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
||||
"""
|
||||
if isinstance(color, str):
|
||||
color = Colors.validate_color(color)
|
||||
color = Colors.hex_to_rgba(color, 255)
|
||||
self.config.color = color
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_max_points(self, max_points: int) -> None:
|
||||
"""
|
||||
Set the maximum number of points to display.
|
||||
|
||||
Args:
|
||||
max_points(int): Maximum number of points to display.
|
||||
"""
|
||||
self.config.max_points = max_points
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
|
||||
Args:
|
||||
precision(int): Decimal precision of the motor position.
|
||||
"""
|
||||
self.config.precision = precision
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_num_dim_points(self, num_dim_points: int) -> None:
|
||||
"""
|
||||
Set the number of dim points for the motor map.
|
||||
|
||||
Args:
|
||||
num_dim_points(int): Number of dim points.
|
||||
"""
|
||||
self.config.num_dim_points = num_dim_points
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_background_value(self, background_value: int) -> None:
|
||||
"""
|
||||
Set the background value of the motor map.
|
||||
|
||||
Args:
|
||||
background_value(int): Background value of the motor map.
|
||||
"""
|
||||
self.config.background_value = background_value
|
||||
self._swap_limit_map()
|
||||
|
||||
def set_scatter_size(self, scatter_size: int) -> None:
|
||||
"""
|
||||
Set the scatter size of the motor map plot.
|
||||
|
||||
Args:
|
||||
scatter_size(int): Size of the scatter points.
|
||||
"""
|
||||
self.config.scatter_size = scatter_size
|
||||
self.update_signal.emit()
|
||||
|
||||
def _disconnect_current_motors(self):
|
||||
"""Disconnect the current motors from the slots."""
|
||||
if self.motor_x is not None and self.motor_y is not None:
|
||||
endpoints = [
|
||||
MessageEndpoints.device_readback(self.motor_x),
|
||||
MessageEndpoints.device_readback(self.motor_y),
|
||||
]
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _connect_motor_to_slots(self):
|
||||
"""Connect motors to slots."""
|
||||
self._disconnect_current_motors()
|
||||
|
||||
self.motor_x = self.config.signals.x.name
|
||||
self.motor_y = self.config.signals.y.name
|
||||
|
||||
endpoints = [
|
||||
MessageEndpoints.device_readback(self.motor_x),
|
||||
MessageEndpoints.device_readback(self.motor_y),
|
||||
]
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _swap_limit_map(self):
|
||||
"""Swap the limit map."""
|
||||
self.plot_item.removeItem(self.plot_components["limit_map"])
|
||||
if self.config.signals.x.limits is not None and self.config.signals.y.limits is not None:
|
||||
self.plot_components["limit_map"] = self._make_limit_map(
|
||||
self.config.signals.x.limits, self.config.signals.y.limits
|
||||
)
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
|
||||
def _make_motor_map(self):
|
||||
"""
|
||||
Create the motor map plot.
|
||||
"""
|
||||
# Create limit map
|
||||
motor_x_limit = self.config.signals.x.limits
|
||||
motor_y_limit = self.config.signals.y.limits
|
||||
if motor_x_limit is not None or motor_y_limit is not None:
|
||||
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
|
||||
# Create scatter plot
|
||||
scatter_size = self.config.scatter_size
|
||||
self.plot_components["scatter"] = pg.ScatterPlotItem(
|
||||
size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255)
|
||||
)
|
||||
self.plot_item.addItem(self.plot_components["scatter"])
|
||||
self.plot_components["scatter"].setZValue(0)
|
||||
|
||||
# Enable Grid
|
||||
self.set_grid(True, True)
|
||||
|
||||
# Add the crosshair for initial motor coordinates
|
||||
initial_position_x = self._get_motor_init_position(
|
||||
self.motor_x, self.config.signals.x.entry, self.config.precision
|
||||
)
|
||||
initial_position_y = self._get_motor_init_position(
|
||||
self.motor_y, self.config.signals.y.entry, self.config.precision
|
||||
)
|
||||
|
||||
self.database_buffer["x"] = [initial_position_x]
|
||||
self.database_buffer["y"] = [initial_position_y]
|
||||
|
||||
self.plot_components["scatter"].setData([initial_position_x], [initial_position_y])
|
||||
self._add_coordinantes_crosshair(initial_position_x, initial_position_y)
|
||||
|
||||
# Set default labels for the plot
|
||||
self.set(x_label=f"Motor X ({self.motor_x})", y_label=f"Motor Y ({self.motor_y})")
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def _add_coordinantes_crosshair(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Add crosshair to the plot to highlight the current position.
|
||||
|
||||
Args:
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
"""
|
||||
|
||||
# Crosshair to highlight the current position
|
||||
highlight_H = pg.InfiniteLine(
|
||||
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
highlight_V = pg.InfiniteLine(
|
||||
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
|
||||
# Add crosshair to the curve list for future referencing
|
||||
self.plot_components["highlight_H"] = highlight_H
|
||||
self.plot_components["highlight_V"] = highlight_V
|
||||
|
||||
# Add crosshair to the plot
|
||||
self.plot_item.addItem(highlight_H)
|
||||
self.plot_item.addItem(highlight_V)
|
||||
|
||||
highlight_V.setPos(x)
|
||||
highlight_H.setPos(y)
|
||||
|
||||
def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem:
|
||||
"""
|
||||
Create a limit map for the motor map plot.
|
||||
|
||||
Args:
|
||||
limits_x(list): Motor limits for the x axis.
|
||||
limits_y(list): Motor limits for the y axis.
|
||||
|
||||
Returns:
|
||||
pg.ImageItem: Limit map.
|
||||
"""
|
||||
limit_x_min, limit_x_max = limits_x
|
||||
limit_y_min, limit_y_max = limits_y
|
||||
|
||||
map_width = int(limit_x_max - limit_x_min + 1)
|
||||
map_height = int(limit_y_max - limit_y_min + 1)
|
||||
|
||||
# Create limits map
|
||||
background_value = self.config.background_value
|
||||
limit_map_data = np.full((map_width, map_height), background_value, dtype=np.float32)
|
||||
limit_map = pg.ImageItem()
|
||||
limit_map.setImage(limit_map_data)
|
||||
|
||||
# Translate and scale the image item to match the motor coordinates
|
||||
tr = QtGui.QTransform()
|
||||
tr.translate(limit_x_min, limit_y_min)
|
||||
limit_map.setTransform(tr)
|
||||
|
||||
return limit_map
|
||||
|
||||
def _get_motor_init_position(self, name: str, entry: str, precision: int) -> float:
|
||||
"""
|
||||
Get the motor initial position from the config.
|
||||
|
||||
Args:
|
||||
name(str): Motor name.
|
||||
entry(str): Motor entry.
|
||||
precision(int): Decimal precision of the motor position.
|
||||
|
||||
Returns:
|
||||
float: Motor initial position.
|
||||
"""
|
||||
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
||||
return init_position
|
||||
|
||||
def _validate_signal_entries(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
x_entry: str | None,
|
||||
y_entry: str | None,
|
||||
validate_bec: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Validate the signal name and entry.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
x_entry(str|None): Entry of the x signal.
|
||||
y_entry(str|None): Entry of the y signal.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
|
||||
Returns:
|
||||
tuple[str,str]: Validated x and y entries.
|
||||
"""
|
||||
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)
|
||||
else:
|
||||
x_entry = x_name if x_entry is None else x_entry
|
||||
y_entry = y_name if y_entry is None else y_entry
|
||||
return x_entry, y_entry
|
||||
|
||||
def _get_motor_limit(self, motor: str) -> Union[list | None]: # TODO check if works correctly
|
||||
"""
|
||||
Get the motor limit from the config.
|
||||
|
||||
Args:
|
||||
motor(str): Motor name.
|
||||
|
||||
Returns:
|
||||
float: Motor limit.
|
||||
"""
|
||||
try:
|
||||
limits = self.dev[motor].limits
|
||||
if limits == [0, 0]:
|
||||
return None
|
||||
return limits
|
||||
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
||||
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
||||
logger.error(f"The device '{motor}' does not have defined limits.")
|
||||
return None
|
||||
|
||||
@Slot()
|
||||
def _update_plot(self, _=None):
|
||||
"""Update the motor map plot."""
|
||||
# If the number of points exceeds max_points, delete the oldest points
|
||||
if len(self.database_buffer["x"]) > self.config.max_points:
|
||||
self.database_buffer["x"] = self.database_buffer["x"][-self.config.max_points :]
|
||||
self.database_buffer["y"] = self.database_buffer["y"][-self.config.max_points :]
|
||||
|
||||
x = self.database_buffer["x"]
|
||||
y = self.database_buffer["y"]
|
||||
|
||||
# Setup gradient brush for history
|
||||
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
||||
|
||||
# RGB color
|
||||
r, g, b, a = self.config.color
|
||||
|
||||
# Calculate the decrement step based on self.num_dim_points
|
||||
num_dim_points = self.config.num_dim_points
|
||||
decrement_step = (255 - 50) / num_dim_points
|
||||
|
||||
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
||||
brightness = max(60, 255 - decrement_step * (i - 1))
|
||||
dim_r = int(r * (brightness / 255))
|
||||
dim_g = int(g * (brightness / 255))
|
||||
dim_b = int(b * (brightness / 255))
|
||||
brushes[-i] = pg.mkBrush(dim_r, dim_g, dim_b, a)
|
||||
brushes[-1] = pg.mkBrush(r, g, b, a) # Newest point is always full brightness
|
||||
scatter_size = self.config.scatter_size
|
||||
|
||||
# Update the scatter plot
|
||||
self.plot_components["scatter"].setData(
|
||||
x=x, y=y, brush=brushes, pen=None, size=scatter_size
|
||||
)
|
||||
|
||||
# Get last know position for crosshair
|
||||
current_x = x[-1]
|
||||
current_y = y[-1]
|
||||
|
||||
# Update the crosshair
|
||||
self.plot_components["highlight_V"].setPos(current_x)
|
||||
self.plot_components["highlight_H"].setPos(current_y)
|
||||
|
||||
# TODO not update title but some label
|
||||
# Update plot title
|
||||
precision = self.config.precision
|
||||
self.set_title(
|
||||
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
|
||||
)
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_device_readback(self, msg: dict, metadata: dict) -> None:
|
||||
"""
|
||||
Update the motor map plot with the new motor position.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the device readback.
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
if self.motor_x is None or self.motor_y is None:
|
||||
return
|
||||
|
||||
if self.motor_x in msg["signals"]:
|
||||
x = msg["signals"][self.motor_x]["value"]
|
||||
self.database_buffer["x"].append(x)
|
||||
self.database_buffer["y"].append(self.database_buffer["y"][-1])
|
||||
|
||||
elif self.motor_y in msg["signals"]:
|
||||
y = msg["signals"][self.motor_y]["value"]
|
||||
self.database_buffer["y"].append(y)
|
||||
self.database_buffer["x"].append(self.database_buffer["x"][-1])
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self._disconnect_current_motors()
|
@ -1,340 +0,0 @@
|
||||
from collections import deque
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, field_validator
|
||||
from pyqtgraph.exporters import MatplotlibExporter
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class BECMultiWaveformConfig(SubplotConfig):
|
||||
color_palette: Optional[str] = Field(
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
curve_limit: Optional[int] = Field(
|
||||
200, description="The maximum number of curves to display on the plot."
|
||||
)
|
||||
flush_buffer: Optional[bool] = Field(
|
||||
False, description="Flush the buffer of the plot widget when the curve limit is reached."
|
||||
)
|
||||
monitor: Optional[str] = Field(
|
||||
None, description="The monitor to set for the plot widget."
|
||||
) # TODO validate monitor in bec -> maybe make it as SignalData class for validation purpose
|
||||
curve_width: Optional[int] = Field(1, description="The width of the curve on the plot.")
|
||||
opacity: Optional[int] = Field(50, description="The opacity of the curve on the plot.")
|
||||
highlight_last_curve: Optional[bool] = Field(
|
||||
True, description="Highlight the last curve on the plot."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
_validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class BECMultiWaveform(BECPlotBase):
|
||||
monitor_signal_updated = Signal()
|
||||
highlighted_curve_index_changed = Signal(int)
|
||||
USER_ACCESS = [
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"curves",
|
||||
"set_monitor",
|
||||
"set_opacity",
|
||||
"set_curve_limit",
|
||||
"set_curve_highlight",
|
||||
"set_colormap",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_colormap",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"get_all_data",
|
||||
"remove",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[BECMultiWaveformConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = BECMultiWaveformConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
self.monitor = None
|
||||
self.connected = False
|
||||
self.current_highlight_index = 0
|
||||
self._curves = deque()
|
||||
self.visible_curves = []
|
||||
self.number_of_visible_curves = 0
|
||||
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@property
|
||||
def curves(self) -> deque:
|
||||
"""
|
||||
Get the curves of the plot widget as a deque.
|
||||
Returns:
|
||||
deque: Deque of curves.
|
||||
"""
|
||||
return self._curves
|
||||
|
||||
@curves.setter
|
||||
def curves(self, value: deque):
|
||||
self._curves = value
|
||||
|
||||
@property
|
||||
def highlight_last_curve(self) -> bool:
|
||||
"""
|
||||
Get the highlight_last_curve property.
|
||||
Returns:
|
||||
bool: The highlight_last_curve property.
|
||||
"""
|
||||
return self.config.highlight_last_curve
|
||||
|
||||
@highlight_last_curve.setter
|
||||
def highlight_last_curve(self, value: bool):
|
||||
self.config.highlight_last_curve = value
|
||||
|
||||
def set_monitor(self, monitor: str):
|
||||
"""
|
||||
Set the monitor for the plot widget.
|
||||
Args:
|
||||
monitor (str): The monitor to set.
|
||||
"""
|
||||
self.config.monitor = monitor
|
||||
self._connect_monitor()
|
||||
|
||||
def _connect_monitor(self):
|
||||
"""
|
||||
Connect the monitor to the plot widget.
|
||||
"""
|
||||
try:
|
||||
previous_monitor = self.monitor
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
|
||||
if previous_monitor and self.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||
)
|
||||
if self.config.monitor and self.connected is False:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(self.config.monitor)
|
||||
)
|
||||
self.connected = True
|
||||
self.monitor = self.config.monitor
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_monitor_1d_update(self, msg: dict, metadata: dict):
|
||||
"""
|
||||
Update the plot widget with the monitor data.
|
||||
|
||||
Args:
|
||||
msg(dict): The message data.
|
||||
metadata(dict): The metadata of the message.
|
||||
"""
|
||||
data = msg.get("data", None)
|
||||
current_scan_id = metadata.get("scan_id", None)
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.scan_id = current_scan_id
|
||||
self.clear_curves()
|
||||
self.curves.clear()
|
||||
if self.crosshair:
|
||||
self.crosshair.clear_markers()
|
||||
|
||||
# Always create a new curve and add it
|
||||
curve = pg.PlotDataItem()
|
||||
curve.setData(data)
|
||||
self.plot_item.addItem(curve)
|
||||
self.curves.append(curve)
|
||||
|
||||
# Max Trace and scale colors
|
||||
self.set_curve_limit(self.config.curve_limit, self.config.flush_buffer)
|
||||
|
||||
self.monitor_signal_updated.emit()
|
||||
|
||||
@Slot(int)
|
||||
def set_curve_highlight(self, index: int):
|
||||
"""
|
||||
Set the curve highlight based on visible curves.
|
||||
|
||||
Args:
|
||||
index (int): The index of the curve to highlight among visible curves.
|
||||
"""
|
||||
self.plot_item.visible_curves = [curve for curve in self.curves if curve.isVisible()]
|
||||
num_visible_curves = len(self.plot_item.visible_curves)
|
||||
self.number_of_visible_curves = num_visible_curves
|
||||
|
||||
if num_visible_curves == 0:
|
||||
return # No curves to highlight
|
||||
|
||||
if index >= num_visible_curves:
|
||||
index = num_visible_curves - 1
|
||||
elif index < 0:
|
||||
index = num_visible_curves + index
|
||||
self.current_highlight_index = index
|
||||
num_colors = num_visible_curves
|
||||
colors = Colors.evenly_spaced_colors(
|
||||
colormap=self.config.color_palette, num=num_colors, format="HEX"
|
||||
)
|
||||
for i, curve in enumerate(self.plot_item.visible_curves):
|
||||
curve.setPen()
|
||||
if i == self.current_highlight_index:
|
||||
curve.setPen(pg.mkPen(color=colors[i], width=5))
|
||||
curve.setAlpha(alpha=1, auto=False)
|
||||
curve.setZValue(1)
|
||||
else:
|
||||
curve.setPen(pg.mkPen(color=colors[i], width=1))
|
||||
curve.setAlpha(alpha=self.config.opacity / 100, auto=False)
|
||||
curve.setZValue(0)
|
||||
|
||||
self.highlighted_curve_index_changed.emit(self.current_highlight_index)
|
||||
|
||||
@Slot(int)
|
||||
def set_opacity(self, opacity: int):
|
||||
"""
|
||||
Set the opacity of the curve on the plot.
|
||||
|
||||
Args:
|
||||
opacity(int): The opacity of the curve. 0-100.
|
||||
"""
|
||||
self.config.opacity = max(0, min(100, opacity))
|
||||
self.set_curve_highlight(self.current_highlight_index)
|
||||
|
||||
@Slot(int, bool)
|
||||
def set_curve_limit(self, max_trace: int, flush_buffer: bool = False):
|
||||
"""
|
||||
Set the maximum number of traces to display on the plot.
|
||||
|
||||
Args:
|
||||
max_trace (int): The maximum number of traces to display.
|
||||
flush_buffer (bool): Flush the buffer.
|
||||
"""
|
||||
self.config.curve_limit = max_trace
|
||||
self.config.flush_buffer = flush_buffer
|
||||
|
||||
if self.config.curve_limit is None:
|
||||
self.scale_colors()
|
||||
return
|
||||
|
||||
if self.config.flush_buffer:
|
||||
# Remove excess curves from the plot and the deque
|
||||
while len(self.curves) > self.config.curve_limit:
|
||||
curve = self.curves.popleft()
|
||||
self.plot_item.removeItem(curve)
|
||||
else:
|
||||
# Hide or show curves based on the new max_trace
|
||||
num_curves_to_show = min(self.config.curve_limit, len(self.curves))
|
||||
for i, curve in enumerate(self.curves):
|
||||
if i < len(self.curves) - num_curves_to_show:
|
||||
curve.hide()
|
||||
else:
|
||||
curve.show()
|
||||
self.scale_colors()
|
||||
|
||||
def scale_colors(self):
|
||||
"""
|
||||
Scale the colors of the curves based on the current colormap.
|
||||
"""
|
||||
if self.config.highlight_last_curve:
|
||||
self.set_curve_highlight(-1) # Use -1 to highlight the last visible curve
|
||||
else:
|
||||
self.set_curve_highlight(self.current_highlight_index)
|
||||
|
||||
def set_colormap(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the curves.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the curves.
|
||||
"""
|
||||
self.config.color_palette = colormap
|
||||
self.set_curve_highlight(self.current_highlight_index)
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
super().hook_crosshair()
|
||||
if self.crosshair:
|
||||
self.highlighted_curve_index_changed.connect(self.crosshair.update_highlighted_curve)
|
||||
if self.curves:
|
||||
self.crosshair.update_highlighted_curve(self.current_highlight_index)
|
||||
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict:
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
Args:
|
||||
output (Literal["dict", "pandas"]): Format of the output data.
|
||||
|
||||
Returns:
|
||||
dict | pd.DataFrame: Data of all curves in the specified format.
|
||||
"""
|
||||
data = {}
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
pd = None
|
||||
if output == "pandas":
|
||||
logger.warning(
|
||||
"Pandas is not installed. "
|
||||
"Please install pandas using 'pip install pandas'."
|
||||
"Output will be dictionary instead."
|
||||
)
|
||||
output = "dict"
|
||||
|
||||
curve_keys = []
|
||||
curves_list = list(self.curves)
|
||||
for i, curve in enumerate(curves_list):
|
||||
x_data, y_data = curve.getData()
|
||||
if x_data is not None or y_data is not None:
|
||||
key = f"curve_{i}"
|
||||
curve_keys.append(key)
|
||||
if output == "dict":
|
||||
data[key] = {"x": x_data.tolist(), "y": y_data.tolist()}
|
||||
elif output == "pandas" and pd is not None:
|
||||
data[key] = pd.DataFrame({"x": x_data, "y": y_data})
|
||||
|
||||
if output == "pandas" and pd is not None:
|
||||
combined_data = pd.concat([data[key] for key in curve_keys], axis=1, keys=curve_keys)
|
||||
return combined_data
|
||||
return data
|
||||
|
||||
def clear_curves(self):
|
||||
"""
|
||||
Remove all curves from the plot, excluding crosshair items.
|
||||
"""
|
||||
items_to_remove = []
|
||||
for item in self.plot_item.items:
|
||||
if not getattr(item, "is_crosshair", False) and isinstance(item, pg.PlotDataItem):
|
||||
items_to_remove.append(item)
|
||||
for item in items_to_remove:
|
||||
self.plot_item.removeItem(item)
|
||||
|
||||
def export_to_matplotlib(self):
|
||||
"""
|
||||
Export current waveform to matplotlib GUI. Available only if matplotlib is installed in the environment.
|
||||
"""
|
||||
MatplotlibExporter(self.plot_item).export()
|
@ -1,505 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
import bec_qthemes
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.crosshair import Crosshair
|
||||
from bec_widgets.utils.fps_counter import FPSCounter
|
||||
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
title: Optional[str] = Field(None, description="The title of the axes.")
|
||||
title_size: Optional[int] = Field(None, description="The font size of the title.")
|
||||
x_label: Optional[str] = Field(None, description="The label for the x-axis.")
|
||||
x_label_size: Optional[int] = Field(None, description="The font size of the x-axis label.")
|
||||
y_label: Optional[str] = Field(None, description="The label for the y-axis.")
|
||||
y_label_size: Optional[int] = Field(None, description="The font size of the y-axis label.")
|
||||
legend_label_size: Optional[int] = Field(
|
||||
None, description="The font size of the legend labels."
|
||||
)
|
||||
x_scale: Literal["linear", "log"] = Field("linear", description="The scale of the x-axis.")
|
||||
y_scale: Literal["linear", "log"] = Field("linear", description="The scale of the y-axis.")
|
||||
x_lim: Optional[tuple] = Field(None, description="The limits of the x-axis.")
|
||||
y_lim: Optional[tuple] = Field(None, description="The limits of the y-axis.")
|
||||
x_grid: bool = Field(False, description="Show grid on the x-axis.")
|
||||
y_grid: bool = Field(False, description="Show grid on the y-axis.")
|
||||
outer_axes: bool = Field(False, description="Show the outer axes of the plot widget.")
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class SubplotConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent figure of the plot.")
|
||||
|
||||
# Coordinates in the figure
|
||||
row: int = Field(0, description="The row coordinate in the figure.")
|
||||
col: int = Field(0, description="The column coordinate in the figure.")
|
||||
|
||||
# Appearance settings
|
||||
axis: AxisConfig = Field(
|
||||
default_factory=AxisConfig, description="The axis configuration of the plot."
|
||||
)
|
||||
|
||||
|
||||
class BECViewBox(pg.ViewBox):
|
||||
sigPaint = Signal()
|
||||
|
||||
def paint(self, painter, opt, widget):
|
||||
super().paint(painter, opt, widget)
|
||||
self.sigPaint.emit()
|
||||
|
||||
def itemBoundsChanged(self, item):
|
||||
self._itemBoundsCache.pop(item, None)
|
||||
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
||||
# check if the call is coming from a mouse-move event
|
||||
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
||||
return
|
||||
self._autoRangeNeedsUpdate = True
|
||||
self.update()
|
||||
|
||||
|
||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
crosshair_position_changed = Signal(tuple)
|
||||
crosshair_position_clicked = Signal(tuple)
|
||||
crosshair_coordinates_changed = Signal(tuple)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
USER_ACCESS = [
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_outer_axes",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
"set_legend_label_size",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None, # TODO decide if needed for this class
|
||||
parent_figure=None,
|
||||
config: Optional[SubplotConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = SubplotConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
pg.GraphicsLayout.__init__(self, parent)
|
||||
|
||||
self.figure = parent_figure
|
||||
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
|
||||
self.addItem(self.plot_item, row=1, col=0)
|
||||
|
||||
self.add_legend()
|
||||
self.crosshair = None
|
||||
self.fps_monitor = None
|
||||
self.fps_label = None
|
||||
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
||||
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
|
||||
self._connect_to_theme_change()
|
||||
|
||||
def _connect_to_theme_change(self):
|
||||
"""Connect to the theme change signal."""
|
||||
qapp = QApplication.instance()
|
||||
if hasattr(qapp, "theme_signal"):
|
||||
qapp.theme_signal.theme_updated.connect(self._update_theme)
|
||||
|
||||
@Slot(str)
|
||||
def _update_theme(self, theme: str):
|
||||
"""Update the theme."""
|
||||
if theme is None:
|
||||
qapp = QApplication.instance()
|
||||
if hasattr(qapp, "theme"):
|
||||
theme = qapp.theme.theme
|
||||
else:
|
||||
theme = "dark"
|
||||
self.apply_theme(theme)
|
||||
|
||||
def apply_theme(self, theme: str):
|
||||
"""
|
||||
Apply the theme to the plot widget.
|
||||
|
||||
Args:
|
||||
theme(str, optional): The theme to be applied.
|
||||
"""
|
||||
palette = bec_qthemes.load_palette(theme)
|
||||
text_pen = pg.mkPen(color=palette.text().color())
|
||||
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
self.plot_item.getAxis(axis).setPen(text_pen)
|
||||
self.plot_item.getAxis(axis).setTextPen(text_pen)
|
||||
if self.plot_item.legend is not None:
|
||||
for sample, label in self.plot_item.legend.items:
|
||||
label.setText(label.text, color=palette.text().color())
|
||||
|
||||
def set(self, **kwargs) -> None:
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"title": self.set_title,
|
||||
"x_label": self.set_x_label,
|
||||
"y_label": self.set_y_label,
|
||||
"x_scale": self.set_x_scale,
|
||||
"y_scale": self.set_y_scale,
|
||||
"x_lim": self.set_x_lim,
|
||||
"y_lim": self.set_y_lim,
|
||||
"legend_label_size": self.set_legend_label_size,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
logger.warning(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def apply_axis_config(self):
|
||||
"""Apply the axis configuration to the plot widget."""
|
||||
config_mappings = {
|
||||
"title": self.config.axis.title,
|
||||
"x_label": self.config.axis.x_label,
|
||||
"y_label": self.config.axis.y_label,
|
||||
"x_scale": self.config.axis.x_scale,
|
||||
"y_scale": self.config.axis.y_scale,
|
||||
"x_lim": self.config.axis.x_lim,
|
||||
"y_lim": self.config.axis.y_lim,
|
||||
}
|
||||
|
||||
self.set(**{k: v for k, v in config_mappings.items() if v is not None})
|
||||
|
||||
def set_legend_label_size(self, size: int = None):
|
||||
"""
|
||||
Set the font size of the legend.
|
||||
|
||||
Args:
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
if not self.plot_item.legend:
|
||||
return
|
||||
if self.config.axis.legend_label_size or size:
|
||||
if size:
|
||||
self.config.axis.legend_label_size = size
|
||||
scale = (
|
||||
size / 9
|
||||
) # 9 is the default font size of the legend, so we always scale it against 9
|
||||
self.plot_item.legend.setScale(scale)
|
||||
|
||||
def get_text_color(self):
|
||||
return "#FFF" if self.figure.config.theme == "dark" else "#000"
|
||||
|
||||
def set_title(self, title: str, size: int = None):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
size(int): Font size of the title.
|
||||
"""
|
||||
if self.config.axis.title_size or size:
|
||||
if size:
|
||||
self.config.axis.title_size = size
|
||||
style = {"color": self.get_text_color(), "size": f"{self.config.axis.title_size}pt"}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setTitle(title, **style)
|
||||
self.config.axis.title = title
|
||||
|
||||
def set_x_label(self, label: str, size: int = None):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
if self.config.axis.x_label_size or size:
|
||||
if size:
|
||||
self.config.axis.x_label_size = size
|
||||
style = {
|
||||
"color": self.get_text_color(),
|
||||
"font-size": f"{self.config.axis.x_label_size}pt",
|
||||
}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setLabel("bottom", label, **style)
|
||||
self.config.axis.x_label = label
|
||||
|
||||
def set_y_label(self, label: str, size: int = None):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
if self.config.axis.y_label_size or size:
|
||||
if size:
|
||||
self.config.axis.y_label_size = size
|
||||
color = self.get_text_color()
|
||||
style = {"color": color, "font-size": f"{self.config.axis.y_label_size}pt"}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setLabel("left", label, **style)
|
||||
self.config.axis.y_label = label
|
||||
|
||||
def set_x_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the x-axis.
|
||||
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the x-axis.
|
||||
"""
|
||||
self.plot_item.setLogMode(x=(scale == "log"))
|
||||
self.config.axis.x_scale = scale
|
||||
|
||||
def set_y_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the y-axis.
|
||||
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the y-axis.
|
||||
"""
|
||||
self.plot_item.setLogMode(y=(scale == "log"))
|
||||
self.config.axis.y_scale = scale
|
||||
|
||||
def set_x_lim(self, *args) -> None:
|
||||
"""
|
||||
Set the limits of the x-axis. This method can accept either two separate arguments
|
||||
for the minimum and maximum x-axis values, or a single tuple containing both limits.
|
||||
|
||||
Usage:
|
||||
set_x_lim(x_min, x_max)
|
||||
set_x_lim((x_min, x_max))
|
||||
|
||||
Args:
|
||||
*args: A variable number of arguments. Can be two integers (x_min and x_max)
|
||||
or a single tuple with two integers.
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], tuple):
|
||||
x_min, x_max = args[0]
|
||||
elif len(args) == 2:
|
||||
x_min, x_max = args
|
||||
else:
|
||||
raise ValueError("set_x_lim expects either two separate arguments or a single tuple")
|
||||
|
||||
self.plot_item.setXRange(x_min, x_max)
|
||||
self.config.axis.x_lim = (x_min, x_max)
|
||||
|
||||
def set_y_lim(self, *args) -> None:
|
||||
"""
|
||||
Set the limits of the y-axis. This method can accept either two separate arguments
|
||||
for the minimum and maximum y-axis values, or a single tuple containing both limits.
|
||||
|
||||
Usage:
|
||||
set_y_lim(y_min, y_max)
|
||||
set_y_lim((y_min, y_max))
|
||||
|
||||
Args:
|
||||
*args: A variable number of arguments. Can be two integers (y_min and y_max)
|
||||
or a single tuple with two integers.
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], tuple):
|
||||
y_min, y_max = args[0]
|
||||
elif len(args) == 2:
|
||||
y_min, y_max = args
|
||||
else:
|
||||
raise ValueError("set_y_lim expects either two separate arguments or a single tuple")
|
||||
|
||||
self.plot_item.setYRange(y_min, y_max)
|
||||
self.config.axis.y_lim = (y_min, y_max)
|
||||
|
||||
def set_grid(self, x: bool = False, y: bool = False):
|
||||
"""
|
||||
Set the grid of the plot widget.
|
||||
|
||||
Args:
|
||||
x(bool): Show grid on the x-axis.
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
self.plot_item.showGrid(x, y)
|
||||
self.config.axis.x_grid = x
|
||||
self.config.axis.y_grid = y
|
||||
|
||||
def set_outer_axes(self, show: bool = True):
|
||||
"""
|
||||
Set the outer axes of the plot widget.
|
||||
|
||||
Args:
|
||||
show(bool): Show the outer axes.
|
||||
"""
|
||||
self.plot_item.showAxis("top", show)
|
||||
self.plot_item.showAxis("right", show)
|
||||
self.config.axis.outer_axes = show
|
||||
|
||||
def add_legend(self):
|
||||
"""Add legend to the plot"""
|
||||
self.plot_item.addLegend()
|
||||
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
Lock aspect ratio.
|
||||
|
||||
Args:
|
||||
lock(bool): True to lock, False to unlock.
|
||||
"""
|
||||
self.plot_item.setAspectLocked(lock)
|
||||
|
||||
def set_auto_range(self, enabled: bool, axis: str = "xy"):
|
||||
"""
|
||||
Set the auto range of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the auto range.
|
||||
axis(str, optional): The axis to enable the auto range.
|
||||
- "xy": Enable auto range for both x and y axis.
|
||||
- "x": Enable auto range for x axis.
|
||||
- "y": Enable auto range for y axis.
|
||||
"""
|
||||
self.plot_item.enableAutoRange(axis, enabled)
|
||||
|
||||
############################################################
|
||||
###################### Crosshair ###########################
|
||||
############################################################
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
"""Hook the crosshair to all plots."""
|
||||
if self.crosshair is None:
|
||||
self.crosshair = Crosshair(self.plot_item, precision=3)
|
||||
self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
|
||||
self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
|
||||
|
||||
def unhook_crosshair(self) -> None:
|
||||
"""Unhook the crosshair from all plots."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.crosshairChanged.disconnect(self.crosshair_position_changed)
|
||||
self.crosshair.crosshairClicked.disconnect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.cleanup()
|
||||
self.crosshair.deleteLater()
|
||||
self.crosshair = None
|
||||
|
||||
def toggle_crosshair(self) -> None:
|
||||
"""Toggle the crosshair on all plots."""
|
||||
if self.crosshair is None:
|
||||
return self.hook_crosshair()
|
||||
|
||||
self.unhook_crosshair()
|
||||
|
||||
@Slot()
|
||||
def reset(self) -> None:
|
||||
"""Reset the plot widget."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.clear_markers()
|
||||
self.crosshair.update_markers()
|
||||
|
||||
############################################################
|
||||
##################### FPS Counter ##########################
|
||||
############################################################
|
||||
|
||||
def update_fps_label(self, fps: float) -> None:
|
||||
"""
|
||||
Update the FPS label.
|
||||
|
||||
Args:
|
||||
fps(float): The frames per second.
|
||||
"""
|
||||
if self.fps_label:
|
||||
self.fps_label.setText(f"FPS: {fps:.2f}")
|
||||
|
||||
def hook_fps_monitor(self):
|
||||
"""Hook the FPS monitor to the plot."""
|
||||
if self.fps_monitor is None:
|
||||
# text_color = self.get_text_color()#TODO later
|
||||
self.fps_monitor = FPSCounter(self.plot_item.vb) # text_color=text_color)
|
||||
self.fps_label = pg.LabelItem(justify="right")
|
||||
self.addItem(self.fps_label, row=0, col=0)
|
||||
|
||||
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
|
||||
|
||||
def unhook_fps_monitor(self, delete_label=True):
|
||||
"""Unhook the FPS monitor from the plot."""
|
||||
if self.fps_monitor is not None:
|
||||
# Remove Monitor
|
||||
self.fps_monitor.cleanup()
|
||||
self.fps_monitor.deleteLater()
|
||||
self.fps_monitor = None
|
||||
if self.fps_label is not None and delete_label:
|
||||
# Remove Label
|
||||
self.removeItem(self.fps_label)
|
||||
self.fps_label.deleteLater()
|
||||
self.fps_label = None
|
||||
|
||||
def enable_fps_monitor(self, enable: bool = True):
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
if enable and self.fps_monitor is None:
|
||||
self.hook_fps_monitor()
|
||||
elif not enable and self.fps_monitor is not None:
|
||||
self.unhook_fps_monitor()
|
||||
|
||||
def export(self):
|
||||
"""Show the Export Dialog of the plot widget."""
|
||||
scene = self.plot_item.scene()
|
||||
scene.contextMenuItem = self.plot_item
|
||||
scene.showExportDialog()
|
||||
|
||||
def remove(self):
|
||||
"""Remove the plot widget from the figure."""
|
||||
if self.figure is not None:
|
||||
self.figure.remove(widget_id=self.gui_id)
|
||||
|
||||
def cleanup_pyqtgraph(self):
|
||||
"""Cleanup pyqtgraph items."""
|
||||
self.unhook_crosshair()
|
||||
self.unhook_fps_monitor(delete_label=False)
|
||||
self.tick_item.cleanup()
|
||||
self.arrow_item.cleanup()
|
||||
item = self.plot_item
|
||||
item.vb.menu.close()
|
||||
item.vb.menu.deleteLater()
|
||||
item.ctrlMenu.close()
|
||||
item.ctrlMenu.deleteLater()
|
File diff suppressed because it is too large
Load Diff
@ -1,277 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy import QtCore
|
||||
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform import BECWaveform1D
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SignalData(BaseModel):
|
||||
"""The data configuration of a signal in the 1D waveform widget for x and y axis."""
|
||||
|
||||
name: str
|
||||
entry: str
|
||||
unit: Optional[str] = None # todo implement later
|
||||
modifier: Optional[str] = None # todo implement later
|
||||
limits: Optional[list[float]] = None # todo implement later
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class Signal(BaseModel):
|
||||
"""The configuration of a signal in the 1D waveform widget."""
|
||||
|
||||
source: str
|
||||
x: Optional[SignalData] = None
|
||||
y: SignalData
|
||||
z: Optional[SignalData] = None
|
||||
dap: Optional[str] = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str | None] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
symbol_size: Optional[int] = Field(7, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(4, description="The width of the pen of the curve.")
|
||||
pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field(
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
color_map_z: Optional[str] = Field(
|
||||
"magma", description="The colormap of the curves z gradient.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color_map_z = field_validator("color_map_z")(Colors.validate_color_map)
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
_validate_symbol_color = field_validator("symbol_color")(Colors.validate_color)
|
||||
|
||||
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"remove",
|
||||
"dap_params",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_color_map_z",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
"dap_params",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_item: Optional[BECWaveform1D] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = CurveConfig(label=name, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
# config.widget_class = self.__class__.__name__
|
||||
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.apply_config()
|
||||
self.dap_params = None
|
||||
self.dap_summary = None
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def apply_config(self):
|
||||
pen_style_map = {
|
||||
"solid": QtCore.Qt.SolidLine,
|
||||
"dash": QtCore.Qt.DashLine,
|
||||
"dot": QtCore.Qt.DotLine,
|
||||
"dashdot": QtCore.Qt.DashDotLine,
|
||||
}
|
||||
pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine)
|
||||
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=pen_style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol:
|
||||
symbol_color = self.config.symbol_color or self.config.color
|
||||
brush = pg.mkBrush(color=symbol_color)
|
||||
|
||||
self.setSymbolBrush(brush)
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
@property
|
||||
def dap_params(self):
|
||||
return self._dap_params
|
||||
|
||||
@dap_params.setter
|
||||
def dap_params(self, value):
|
||||
self._dap_params = value
|
||||
|
||||
@property
|
||||
def dap_summary(self):
|
||||
return self._dap_report
|
||||
|
||||
@dap_summary.setter
|
||||
def dap_summary(self, value):
|
||||
self._dap_report = value
|
||||
|
||||
def set_data(self, x, y):
|
||||
if self.config.source == "custom":
|
||||
self.setData(x, y)
|
||||
else:
|
||||
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"color_map_z": self.set_color_map_z,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
"pen_width": self.set_pen_width,
|
||||
"pen_style": self.set_pen_style,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
logger.warning(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_color(self, color: str, symbol_color: Optional[str] = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
self.config.color = color
|
||||
self.config.symbol_color = symbol_color or color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol(self, symbol: str):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
self.config.symbol = symbol
|
||||
self.setSymbol(symbol)
|
||||
self.updateItems()
|
||||
|
||||
def set_symbol_color(self, symbol_color: str):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
self.config.symbol_color = symbol_color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol_size(self, symbol_size: int):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
self.config.symbol_size = symbol_size
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_width(self, pen_width: int):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
self.config.pen_width = pen_width
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot"]):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_color_map_z(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.color_map_z = colormap
|
||||
self.apply_config()
|
||||
self.parent_item.scan_history(-1)
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
try:
|
||||
x_data, y_data = self.getData()
|
||||
except TypeError:
|
||||
x_data, y_data = np.array([]), np.array([])
|
||||
return x_data, y_data
|
||||
|
||||
def clear_data(self):
|
||||
self.setData([], [])
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
# self.parent_item.removeItem(self)
|
||||
self.parent_item.remove_curve(self.name())
|
||||
super().remove()
|
@ -5,6 +5,8 @@ import random
|
||||
import pytest
|
||||
|
||||
from bec_widgets.cli.client_utils import BECGuiClient
|
||||
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=redefined-outer-name
|
||||
@ -23,6 +25,26 @@ def threads_check_fixture(threads_check):
|
||||
|
||||
@pytest.fixture
|
||||
def gui_id():
|
||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
||||
|
||||
|
||||
@contextmanager
|
||||
def plot_server(gui_id, klass, client_lib):
|
||||
dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client
|
||||
process, _ = _start_plot_process(
|
||||
gui_id, klass, gui_class_id="bec", config=client_lib._client._service_config.config_path
|
||||
)
|
||||
try:
|
||||
while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None:
|
||||
time.sleep(0.3)
|
||||
yield gui_id
|
||||
finally:
|
||||
process.terminate()
|
||||
process.wait()
|
||||
dispatcher.disconnect_all()
|
||||
dispatcher.reset_singleton()
|
||||
|
||||
|
||||
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
|
||||
return f"figure_{random.randint(0,100)}"
|
||||
|
||||
|
@ -1,242 +0,0 @@
|
||||
# import time
|
||||
|
||||
# import numpy as np
|
||||
# import pytest
|
||||
# from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
# from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
# from bec_widgets.cli.rpc.rpc_base import RPCReference
|
||||
# from bec_widgets.tests.utils import check_remote_data_size
|
||||
|
||||
# # pylint: disable=protected-access
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def connected_figure(connected_client_gui_obj):
|
||||
# gui = connected_client_gui_obj
|
||||
# dock = gui.window_list[0].new("dock")
|
||||
# fig = dock.new(name="fig", widget="BECFigure")
|
||||
# return fig
|
||||
|
||||
|
||||
# def test_rpc_waveform1d_custom_curve(connected_figure):
|
||||
# fig = connected_figure
|
||||
|
||||
# ax = fig.plot()
|
||||
# curve = ax.plot(x=[1, 2, 3], y=[1, 2, 3])
|
||||
# curve.set_color("red")
|
||||
# curve = ax.curves[0]
|
||||
# curve.set_color("blue")
|
||||
|
||||
# assert len(fig.widgets) == 1
|
||||
# assert len(fig.widgets[ax._rpc_id].curves) == 1
|
||||
|
||||
|
||||
# def test_rpc_plotting_shortcuts_init_configs(connected_figure, qtbot):
|
||||
# fig = connected_figure
|
||||
|
||||
# plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
# im = fig.image("eiger")
|
||||
# motor_map = fig.motor_map("samx", "samy")
|
||||
# plt_z = fig.plot(x_name="samx", y_name="samy", z_name="bpm4i", new=True)
|
||||
|
||||
# # Checking if classes are correctly initialised
|
||||
# assert len(fig.widgets) == 4
|
||||
# assert plt.__class__.__name__ == "RPCReference"
|
||||
# assert plt.__class__ == RPCReference
|
||||
# assert plt._root._ipython_registry[plt._gui_id].__class__ == BECWaveform
|
||||
# assert im.__class__.__name__ == "RPCReference"
|
||||
# assert im.__class__ == RPCReference
|
||||
# assert im._root._ipython_registry[im._gui_id].__class__ == BECImageShow
|
||||
# assert motor_map.__class__.__name__ == "RPCReference"
|
||||
# assert motor_map.__class__ == RPCReference
|
||||
# assert motor_map._root._ipython_registry[motor_map._gui_id].__class__ == BECMotorMap
|
||||
|
||||
# # check if the correct devices are set
|
||||
# # plot
|
||||
# assert plt._config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
|
||||
# "dap": None,
|
||||
# "source": "scan_segment",
|
||||
# "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
# "y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
# "z": None,
|
||||
# }
|
||||
# # image
|
||||
# assert im._config_dict["images"]["eiger"]["monitor"] == "eiger"
|
||||
# # motor map
|
||||
# assert motor_map._config_dict["signals"] == {
|
||||
# "dap": None,
|
||||
# "source": "device_readback",
|
||||
# "x": {
|
||||
# "name": "samx",
|
||||
# "entry": "samx",
|
||||
# "unit": None,
|
||||
# "modifier": None,
|
||||
# "limits": [-50.0, 50.0],
|
||||
# },
|
||||
# "y": {
|
||||
# "name": "samy",
|
||||
# "entry": "samy",
|
||||
# "unit": None,
|
||||
# "modifier": None,
|
||||
# "limits": [-50.0, 50.0],
|
||||
# },
|
||||
# "z": None,
|
||||
# }
|
||||
# # plot with z scatter
|
||||
# assert plt_z._config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
|
||||
# "dap": None,
|
||||
# "source": "scan_segment",
|
||||
# "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
# "y": {"name": "samy", "entry": "samy", "unit": None, "modifier": None, "limits": None},
|
||||
# "z": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
# }
|
||||
|
||||
|
||||
# def test_rpc_waveform_scan(qtbot, connected_figure, bec_client_lib):
|
||||
# fig = connected_figure
|
||||
# # add 3 different curves to track
|
||||
# plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
# fig.plot(x_name="samx", y_name="bpm3a")
|
||||
# fig.plot(x_name="samx", y_name="bpm4d")
|
||||
|
||||
# client = bec_client_lib
|
||||
# dev = client.device_manager.devices
|
||||
# scans = client.scans
|
||||
# queue = client.queue
|
||||
|
||||
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
# status.wait()
|
||||
|
||||
# item = queue.scan_storage.storage[-1]
|
||||
# last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
|
||||
# num_elements = 10
|
||||
|
||||
# for plot_name in ["bpm4i-bpm4i", "bpm3a-bpm3a", "bpm4d-bpm4d"]:
|
||||
# qtbot.waitUntil(lambda: check_remote_data_size(plt, plot_name, num_elements))
|
||||
|
||||
# # get data from curves
|
||||
# plt_data = plt.get_all_data()
|
||||
|
||||
# # check plotted data
|
||||
# assert plt_data["bpm4i-bpm4i"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
# assert plt_data["bpm4i-bpm4i"]["y"] == last_scan_data["bpm4i"]["bpm4i"].val
|
||||
# assert plt_data["bpm3a-bpm3a"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
# assert plt_data["bpm3a-bpm3a"]["y"] == last_scan_data["bpm3a"]["bpm3a"].val
|
||||
# assert plt_data["bpm4d-bpm4d"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
# assert plt_data["bpm4d-bpm4d"]["y"] == last_scan_data["bpm4d"]["bpm4d"].val
|
||||
|
||||
|
||||
# def test_rpc_image(connected_figure, bec_client_lib):
|
||||
# fig = connected_figure
|
||||
|
||||
# im = fig.image("eiger")
|
||||
|
||||
# client = bec_client_lib
|
||||
# dev = client.device_manager.devices
|
||||
# scans = client.scans
|
||||
|
||||
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
# status.wait()
|
||||
|
||||
# last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
|
||||
# "data"
|
||||
# ].data
|
||||
# last_image_plot = im.images[0].get_data()
|
||||
|
||||
# # check plotted data
|
||||
# np.testing.assert_equal(last_image_device, last_image_plot)
|
||||
|
||||
|
||||
# def test_rpc_motor_map(connected_figure, bec_client_lib):
|
||||
# fig = connected_figure
|
||||
|
||||
# motor_map = fig.motor_map("samx", "samy")
|
||||
|
||||
# client = bec_client_lib
|
||||
# dev = client.device_manager.devices
|
||||
# scans = client.scans
|
||||
|
||||
# initial_pos_x = dev.samx.read()["samx"]["value"]
|
||||
# initial_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
# status = scans.mv(dev.samx, 1, dev.samy, 2, relative=True)
|
||||
# status.wait()
|
||||
|
||||
# final_pos_x = dev.samx.read()["samx"]["value"]
|
||||
# final_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
# # check plotted data
|
||||
# motor_map_data = motor_map.get_data()
|
||||
|
||||
# np.testing.assert_equal(
|
||||
# [motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
|
||||
# )
|
||||
# np.testing.assert_equal(
|
||||
# [motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
|
||||
# )
|
||||
|
||||
|
||||
# def test_dap_rpc(connected_figure, bec_client_lib, qtbot):
|
||||
|
||||
# fig = connected_figure
|
||||
# plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||
|
||||
# client = bec_client_lib
|
||||
# dev = client.device_manager.devices
|
||||
# scans = client.scans
|
||||
|
||||
# dev.bpm4i.sim.select_model("GaussianModel")
|
||||
# params = dev.bpm4i.sim.params
|
||||
# params.update(
|
||||
# {"noise": "uniform", "noise_multiplier": 10, "center": 5, "sigma": 1, "amplitude": 200}
|
||||
# )
|
||||
# dev.bpm4i.sim.params = params
|
||||
# time.sleep(1)
|
||||
|
||||
# res = scans.line_scan(dev.samx, 0, 8, steps=50, relative=False)
|
||||
# res.wait()
|
||||
|
||||
# # especially on slow machines, the fit might not be done yet
|
||||
# # so we wait until the fit reaches the expected value
|
||||
# def wait_for_fit():
|
||||
# dap_curve = plt.get_curve("bpm4i-bpm4i-GaussianModel")
|
||||
# fit_params = dap_curve.dap_params
|
||||
# if fit_params is None:
|
||||
# return False
|
||||
# print(fit_params)
|
||||
# return np.isclose(fit_params["center"], 5, atol=0.5)
|
||||
|
||||
# qtbot.waitUntil(wait_for_fit, timeout=10000)
|
||||
|
||||
# # Repeat fit after adding a region of interest
|
||||
# plt.select_roi(region=(3, 7))
|
||||
# res = scans.line_scan(dev.samx, 0, 8, steps=50, relative=False)
|
||||
# res.wait()
|
||||
|
||||
# qtbot.waitUntil(wait_for_fit, timeout=10000)
|
||||
|
||||
|
||||
# def test_removing_subplots(connected_figure, bec_client_lib):
|
||||
# fig = connected_figure
|
||||
# plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||
# # Registry can't handle multiple subplots on one widget, BECFigure will be deprecated though
|
||||
# # im = fig.image(monitor="eiger")
|
||||
# # mm = fig.motor_map(motor_x="samx", motor_y="samy")
|
||||
|
||||
# assert len(fig.widget_list) == 1
|
||||
|
||||
# # removing curves
|
||||
# assert len(plt.curves) == 2
|
||||
# plt.curves[0].remove()
|
||||
# assert len(plt.curves) == 1
|
||||
# plt.remove_curve("bpm4i-bpm4i")
|
||||
# assert len(plt.curves) == 0
|
||||
|
||||
# # removing all subplots from figure
|
||||
# plt.remove()
|
||||
# # im.remove()
|
||||
# # mm.remove()
|
||||
|
||||
# assert len(fig.widget_list) == 0
|
@ -57,26 +57,6 @@ def test_bec_dock_area_add_remove_dock(bec_dock_area, qtbot):
|
||||
assert d2.name() in dict(bec_dock_area.dock_area.docks)
|
||||
|
||||
|
||||
def test_add_remove_bec_figure_to_dock(bec_dock_area):
|
||||
d0 = bec_dock_area.new()
|
||||
fig = d0.new("BECFigure")
|
||||
plt = fig.plot(x_name="samx", y_name="bpm4i")
|
||||
im = fig.image("eiger")
|
||||
mm = fig.motor_map("samx", "samy")
|
||||
mw = fig.multi_waveform("waveform1d")
|
||||
|
||||
assert len(bec_dock_area.dock_area.docks) == 1
|
||||
assert len(d0.elements) == 1
|
||||
assert len(d0.element_list) == 1
|
||||
assert len(fig.widgets) == 4
|
||||
|
||||
assert fig.config.widget_class == "BECFigure"
|
||||
assert plt.config.widget_class == "BECWaveform"
|
||||
assert im.config.widget_class == "BECImageShow"
|
||||
assert mm.config.widget_class == "BECMotorMap"
|
||||
assert mw.config.widget_class == "BECMultiWaveform"
|
||||
|
||||
|
||||
def test_close_docks(bec_dock_area, qtbot):
|
||||
d0 = bec_dock_area.new(name="dock_0")
|
||||
d1 = bec_dock_area.new(name="dock_1")
|
||||
|
@ -1,275 +0,0 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.figure.plots.image.image import BECImageShow
|
||||
from bec_widgets.widgets.containers.figure.plots.motor_map.motor_map import BECMotorMap
|
||||
from bec_widgets.widgets.containers.figure.plots.multi_waveform.multi_waveform import (
|
||||
BECMultiWaveform,
|
||||
)
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform.waveform import BECWaveform
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_bec_figure_init(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
assert bec_figure is not None
|
||||
assert bec_figure.client is not None
|
||||
assert isinstance(bec_figure, BECFigure)
|
||||
assert bec_figure.config.widget_class == "BECFigure"
|
||||
|
||||
|
||||
def test_bec_figure_init_with_config(mocked_client):
|
||||
config = {"widget_class": "BECFigure", "gui_id": "test_gui_id", "theme": "dark"}
|
||||
widget = BECFigure(client=mocked_client, config=config)
|
||||
assert widget.config.gui_id == "test_gui_id"
|
||||
assert widget.config.theme == "dark"
|
||||
|
||||
|
||||
def test_bec_figure_add_remove_plot(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
initial_count = len(bec_figure._widgets)
|
||||
|
||||
# Adding 3 widgets - 2 WaveformBase and 1 PlotBase
|
||||
w0 = bec_figure.plot(new=True)
|
||||
w1 = bec_figure.plot(new=True)
|
||||
w2 = bec_figure.add_widget(widget_type="BECPlotBase")
|
||||
|
||||
# Check if the widgets were added
|
||||
assert len(bec_figure._widgets) == initial_count + 3
|
||||
assert w0.gui_id in bec_figure._widgets
|
||||
assert w1.gui_id in bec_figure._widgets
|
||||
assert w2.gui_id in bec_figure._widgets
|
||||
assert bec_figure._widgets[w0.gui_id].config.widget_class == "BECWaveform"
|
||||
assert bec_figure._widgets[w1.gui_id].config.widget_class == "BECWaveform"
|
||||
assert bec_figure._widgets[w2.gui_id].config.widget_class == "BECPlotBase"
|
||||
|
||||
# Check accessing positions by the grid in figure
|
||||
assert bec_figure[0, 0] == w0
|
||||
assert bec_figure[1, 0] == w1
|
||||
assert bec_figure[2, 0] == w2
|
||||
|
||||
# Removing 1 widget
|
||||
bec_figure.remove(widget_id=w0.gui_id)
|
||||
assert len(bec_figure._widgets) == initial_count + 2
|
||||
assert w0.gui_id not in bec_figure._widgets
|
||||
assert w2.gui_id in bec_figure._widgets
|
||||
assert bec_figure._widgets[w1.gui_id].config.widget_class == "BECWaveform"
|
||||
|
||||
|
||||
def test_add_different_types_of_widgets(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plt = bec_figure.plot(x_name="samx", y_name="bpm4i")
|
||||
im = bec_figure.image("eiger")
|
||||
motor_map = bec_figure.motor_map("samx", "samy")
|
||||
multi_waveform = bec_figure.multi_waveform("waveform")
|
||||
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.__class__ == BECImageShow
|
||||
assert motor_map.__class__ == BECMotorMap
|
||||
assert multi_waveform.__class__ == BECMultiWaveform
|
||||
|
||||
|
||||
def test_access_widgets_access_errors(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot(row=0, col=0)
|
||||
|
||||
# access widget by non-existent coordinates
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure[0, 2]
|
||||
assert "No widget at coordinates (0, 2)" in str(excinfo.value)
|
||||
|
||||
# access widget by non-existent widget_id
|
||||
with pytest.raises(KeyError) as excinfo:
|
||||
bec_figure["non_existent_widget"]
|
||||
assert "Widget with id 'non_existent_widget' not found" in str(excinfo.value)
|
||||
|
||||
# access widget by wrong type
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
bec_figure[1.2]
|
||||
assert (
|
||||
"Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
|
||||
def test_add_plot_to_occupied_position(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot(row=0, col=0)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure.plot(row=0, col=0, new=True)
|
||||
assert "Position at row 0 and column 0 is already occupied." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_remove_plots(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot(row=0, col=0)
|
||||
w2 = bec_figure.plot(row=0, col=1)
|
||||
w3 = bec_figure.plot(row=1, col=0)
|
||||
w4 = bec_figure.plot(row=1, col=1)
|
||||
|
||||
assert bec_figure[0, 0] == w1
|
||||
assert bec_figure[0, 1] == w2
|
||||
assert bec_figure[1, 0] == w3
|
||||
assert bec_figure[1, 1] == w4
|
||||
|
||||
# remove by coordinates
|
||||
bec_figure[0, 0].remove()
|
||||
assert w1.gui_id not in bec_figure._widgets
|
||||
|
||||
# remove by widget_id
|
||||
bec_figure.remove(widget_id=w2.gui_id)
|
||||
assert w2.gui_id not in bec_figure._widgets
|
||||
|
||||
# remove by widget object
|
||||
w3.remove()
|
||||
assert w3.gui_id not in bec_figure._widgets
|
||||
|
||||
# check the remaining widget 4
|
||||
assert bec_figure[0, 0] == w4
|
||||
assert bec_figure[w4.gui_id] == w4
|
||||
assert w4.gui_id in bec_figure._widgets
|
||||
assert len(bec_figure._widgets) == 1
|
||||
|
||||
|
||||
def test_remove_plots_by_coordinates_ints(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot(row=0, col=0)
|
||||
w2 = bec_figure.plot(row=0, col=1)
|
||||
|
||||
bec_figure.remove(row=0, col=0)
|
||||
assert w1.gui_id not in bec_figure._widgets
|
||||
assert w2.gui_id in bec_figure._widgets
|
||||
assert bec_figure[0, 0] == w2
|
||||
assert len(bec_figure._widgets) == 1
|
||||
|
||||
|
||||
def test_remove_plots_by_coordinates_tuple(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot(row=0, col=0)
|
||||
w2 = bec_figure.plot(row=0, col=1)
|
||||
|
||||
bec_figure.remove(coordinates=(0, 0))
|
||||
assert w1.gui_id not in bec_figure._widgets
|
||||
assert w2.gui_id in bec_figure._widgets
|
||||
assert bec_figure[0, 0] == w2
|
||||
assert len(bec_figure._widgets) == 1
|
||||
|
||||
|
||||
def test_remove_plot_by_id_error(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot()
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure.remove(widget_id="non_existent_widget")
|
||||
assert "Widget with ID 'non_existent_widget' does not exist." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_remove_plot_by_coordinates_error(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot(row=0, col=0)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure.remove(0, 1)
|
||||
assert "No widget at coordinates (0, 1)" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_remove_plot_by_providing_nothing(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot(row=0, col=0)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure.remove()
|
||||
assert "Must provide either widget_id or coordinates for removal." in str(excinfo.value)
|
||||
|
||||
|
||||
# def test_change_theme(bec_figure): #TODO do no work at python 3.12
|
||||
# bec_figure.change_theme("dark")
|
||||
# assert bec_figure.config.theme == "dark"
|
||||
# assert bec_figure.backgroundBrush().color().name() == "#000000"
|
||||
# bec_figure.change_theme("light")
|
||||
# assert bec_figure.config.theme == "light"
|
||||
# assert bec_figure.backgroundBrush().color().name() == "#ffffff"
|
||||
# bec_figure.change_theme("dark")
|
||||
# assert bec_figure.config.theme == "dark"
|
||||
# assert bec_figure.backgroundBrush().color().name() == "#000000"
|
||||
|
||||
|
||||
def test_change_layout(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot(row=0, col=0)
|
||||
w2 = bec_figure.plot(row=0, col=1)
|
||||
w3 = bec_figure.plot(row=1, col=0)
|
||||
w4 = bec_figure.plot(row=1, col=1)
|
||||
|
||||
bec_figure.change_layout(max_columns=1)
|
||||
|
||||
assert np.shape(bec_figure.grid) == (4, 1)
|
||||
assert bec_figure[0, 0] == w1
|
||||
assert bec_figure[1, 0] == w2
|
||||
assert bec_figure[2, 0] == w3
|
||||
assert bec_figure[3, 0] == w4
|
||||
|
||||
bec_figure.change_layout(max_rows=1)
|
||||
|
||||
assert np.shape(bec_figure.grid) == (1, 4)
|
||||
assert bec_figure[0, 0] == w1
|
||||
assert bec_figure[0, 1] == w2
|
||||
assert bec_figure[0, 2] == w3
|
||||
assert bec_figure[0, 3] == w4
|
||||
|
||||
|
||||
def test_clear_all(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
bec_figure.plot(row=0, col=0)
|
||||
bec_figure.plot(row=0, col=1)
|
||||
bec_figure.plot(row=1, col=0)
|
||||
bec_figure.plot(row=1, col=1)
|
||||
|
||||
bec_figure.clear_all()
|
||||
|
||||
assert len(bec_figure._widgets) == 0
|
||||
assert np.shape(bec_figure.grid) == (0,)
|
||||
|
||||
|
||||
def test_shortcuts(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plt = bec_figure.plot(x_name="samx", y_name="bpm4i")
|
||||
im = bec_figure.image("eiger")
|
||||
motor_map = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert plt.config.widget_class == "BECWaveform"
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.config.widget_class == "BECImageShow"
|
||||
assert im.__class__ == BECImageShow
|
||||
assert motor_map.config.widget_class == "BECMotorMap"
|
||||
assert motor_map.__class__ == BECMotorMap
|
||||
|
||||
|
||||
def test_plot_access_factory(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plt_00 = bec_figure.plot(x_name="samx", y_name="bpm4i")
|
||||
plt_01 = bec_figure.plot(x_name="samx", y_name="bpm4i", row=0, col=1)
|
||||
plt_10 = bec_figure.plot(new=True)
|
||||
|
||||
assert bec_figure.widget_list[0] == plt_00
|
||||
assert bec_figure.widget_list[1] == plt_01
|
||||
assert bec_figure.widget_list[2] == plt_10
|
||||
assert bec_figure.axes(row=0, col=0) == plt_00
|
||||
assert bec_figure.axes(row=0, col=1) == plt_01
|
||||
assert bec_figure.axes(row=1, col=0) == plt_10
|
||||
|
||||
assert len(plt_00.curves) == 1
|
||||
assert len(plt_01.curves) == 1
|
||||
assert len(plt_10.curves) == 0
|
||||
|
||||
# update plt_00
|
||||
bec_figure.plot(x_name="samx", y_name="bpm3a")
|
||||
bec_figure.plot(x=[1, 2, 3], y=[1, 2, 3], row=0, col=0)
|
||||
|
||||
assert len(plt_00.curves) == 3
|
@ -1,97 +0,0 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_on_image_update(qtbot, mocked_client):
|
||||
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("eiger")
|
||||
data = np.random.rand(100, 100)
|
||||
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
img = bec_image_show.images[0]
|
||||
assert np.array_equal(img.get_data(), data)
|
||||
|
||||
|
||||
def test_autorange_on_image_update(qtbot, mocked_client):
|
||||
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("eiger")
|
||||
# Check if autorange mode "mean" works, should be default
|
||||
data = np.random.rand(100, 100)
|
||||
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
img = bec_image_show.images[0]
|
||||
assert np.array_equal(img.get_data(), data)
|
||||
vmin = max(np.mean(data) - 2 * np.std(data), 0)
|
||||
vmax = np.mean(data) + 2 * np.std(data)
|
||||
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
|
||||
# Test general update with autorange True, mode "max"
|
||||
bec_image_show.set_autorange_mode("max")
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
img = bec_image_show.images[0]
|
||||
vmin = np.min(data)
|
||||
vmax = np.max(data)
|
||||
assert np.array_equal(img.get_data(), data)
|
||||
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
|
||||
# Change the input data, and switch to autorange False, colormap levels should stay untouched
|
||||
data *= 100
|
||||
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
|
||||
bec_image_show.set_autorange(False)
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
img = bec_image_show.images[0]
|
||||
assert np.array_equal(img.get_data(), data)
|
||||
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-3, 1e-3)).all()
|
||||
# Reactivate autorange, should now scale the new data
|
||||
bec_image_show.set_autorange(True)
|
||||
bec_image_show.set_autorange_mode("mean")
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
img = bec_image_show.images[0]
|
||||
vmin = max(np.mean(data) - 2 * np.std(data), 0)
|
||||
vmax = np.mean(data) + 2 * np.std(data)
|
||||
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
|
||||
|
||||
|
||||
def test_on_image_update_variable_length(qtbot, mocked_client):
|
||||
"""
|
||||
Test the on_image_update slot with data arrays of varying lengths for 'device_monitor_1d' image type.
|
||||
"""
|
||||
# Create the widget and set image_type to 'device_monitor_1d'
|
||||
bec_image_show = create_widget(qtbot, BECFigure, client=mocked_client).image("waveform1d", "1d")
|
||||
|
||||
# Generate data arrays of varying lengths
|
||||
data_lengths = [10, 15, 12, 20, 5, 8, 1, 21]
|
||||
data_arrays = [np.random.rand(length) for length in data_lengths]
|
||||
|
||||
# Simulate sending messages with these data arrays
|
||||
device = "waveform1d"
|
||||
for data in data_arrays:
|
||||
msg = messages.DeviceMonitor1DMessage(
|
||||
device=device, data=data, metadata={"scan_id": "12345"}
|
||||
)
|
||||
bec_image_show.on_image_update(msg.content, msg.metadata)
|
||||
|
||||
# After processing all data, retrieve the image and its data
|
||||
img = bec_image_show.images[0]
|
||||
image_buffer = img.get_data()
|
||||
|
||||
# The image_buffer should be a 2D array with number of rows equal to number of data arrays
|
||||
# and number of columns equal to the maximum data length
|
||||
expected_num_rows = len(data_arrays)
|
||||
expected_num_cols = max(data_lengths)
|
||||
assert image_buffer.shape == (
|
||||
expected_num_rows,
|
||||
expected_num_cols,
|
||||
), f"Expected image buffer shape {(expected_num_rows, expected_num_cols)}, got {image_buffer.shape}"
|
||||
|
||||
# Check that each row in image_buffer corresponds to the padded data arrays
|
||||
for i, data in enumerate(data_arrays):
|
||||
padded_data = np.pad(
|
||||
data, (0, expected_num_cols - len(data)), mode="constant", constant_values=0
|
||||
)
|
||||
assert np.array_equal(
|
||||
image_buffer[i], padded_data
|
||||
), f"Row {i} in image buffer does not match expected padded data"
|
@ -1,282 +0,0 @@
|
||||
import numpy as np
|
||||
from bec_lib.messages import DeviceMessage
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.figure.plots.motor_map.motor_map import MotorMapConfig
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform.waveform_curve import SignalData
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_motor_map_init(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
default_config = MotorMapConfig(widget_class="BECMotorMap")
|
||||
|
||||
mm = bec_figure.motor_map(config=default_config.model_dump())
|
||||
default_config.gui_id = mm.gui_id
|
||||
|
||||
assert mm.config == default_config
|
||||
|
||||
|
||||
def test_motor_map_change_motors(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert mm.motor_x == "samx"
|
||||
assert mm.motor_y == "samy"
|
||||
assert mm.config.signals.x == SignalData(name="samx", entry="samx", limits=[-10, 10])
|
||||
assert mm.config.signals.y == SignalData(name="samy", entry="samy", limits=[-5, 5])
|
||||
|
||||
mm.change_motors("samx", "samz")
|
||||
|
||||
assert mm.config.signals.x == SignalData(name="samx", entry="samx", limits=[-10, 10])
|
||||
assert mm.config.signals.y == SignalData(name="samz", entry="samz", limits=[-8, 8])
|
||||
|
||||
|
||||
def test_motor_map_get_limits(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
expected_limits = {"samx": [-10, 10], "samy": [-5, 5]}
|
||||
|
||||
for motor_name, expected_limit in expected_limits.items():
|
||||
actual_limit = mm._get_motor_limit(motor_name)
|
||||
assert actual_limit == expected_limit
|
||||
|
||||
|
||||
def test_motor_map_get_init_position(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
mm.set_precision(2)
|
||||
|
||||
motor_map_dev = mm.client.device_manager.devices
|
||||
|
||||
expected_positions = {
|
||||
("samx", "samx"): motor_map_dev["samx"].read()["samx"]["value"],
|
||||
("samy", "samy"): motor_map_dev["samy"].read()["samy"]["value"],
|
||||
}
|
||||
|
||||
for (motor_name, entry), expected_position in expected_positions.items():
|
||||
actual_position = mm._get_motor_init_position(motor_name, entry, 2)
|
||||
assert actual_position == expected_position
|
||||
|
||||
|
||||
def test_motor_movement_updates_position_and_database(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
motor_map_dev = mm.client.device_manager.devices
|
||||
|
||||
init_positions = {
|
||||
"samx": [motor_map_dev["samx"].read()["samx"]["value"]],
|
||||
"samy": [motor_map_dev["samy"].read()["samy"]["value"]],
|
||||
}
|
||||
|
||||
mm.change_motors("samx", "samy")
|
||||
|
||||
assert mm.database_buffer["x"] == init_positions["samx"]
|
||||
assert mm.database_buffer["y"] == init_positions["samy"]
|
||||
|
||||
# Simulate motor movement for 'samx' only
|
||||
new_position_samx = 4.0
|
||||
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
|
||||
init_positions["samx"].append(new_position_samx)
|
||||
init_positions["samy"].append(init_positions["samy"][-1])
|
||||
# Verify database update for 'samx'
|
||||
assert mm.database_buffer["x"] == init_positions["samx"]
|
||||
|
||||
# Verify 'samy' retains its last known position
|
||||
assert mm.database_buffer["y"] == init_positions["samy"]
|
||||
|
||||
|
||||
def test_scatter_plot_rendering(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
motor_map_dev = mm.client.device_manager.devices
|
||||
|
||||
init_positions = {
|
||||
"samx": [motor_map_dev["samx"].read()["samx"]["value"]],
|
||||
"samy": [motor_map_dev["samy"].read()["samy"]["value"]],
|
||||
}
|
||||
|
||||
mm.change_motors("samx", "samy")
|
||||
|
||||
# Simulate motor movement for 'samx' only
|
||||
new_position_samx = 4.0
|
||||
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
mm._update_plot()
|
||||
|
||||
# Get the scatter plot item
|
||||
scatter_plot_item = mm.plot_components["scatter"]
|
||||
|
||||
# Check the scatter plot item properties
|
||||
assert len(scatter_plot_item.data) > 0, "Scatter plot data is empty"
|
||||
x_data = scatter_plot_item.data["x"]
|
||||
y_data = scatter_plot_item.data["y"]
|
||||
assert x_data[-1] == new_position_samx, "Scatter plot X data not updated correctly"
|
||||
assert (
|
||||
y_data[-1] == init_positions["samy"][-1]
|
||||
), "Scatter plot Y data should retain last known position"
|
||||
|
||||
|
||||
def test_plot_visualization_consistency(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
mm.change_motors("samx", "samy")
|
||||
# Simulate updating the plot with new data
|
||||
msg = DeviceMessage(signals={"samx": {"value": 5}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
msg = DeviceMessage(signals={"samy": {"value": 9}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
mm._update_plot()
|
||||
|
||||
scatter_plot_item = mm.plot_components["scatter"]
|
||||
|
||||
# Check if the scatter plot reflects the new data correctly
|
||||
assert (
|
||||
scatter_plot_item.data["x"][-1] == 5 and scatter_plot_item.data["y"][-1] == 9
|
||||
), "Plot not updated correctly with new data"
|
||||
|
||||
|
||||
def test_change_background_value(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert mm.config.background_value == 25
|
||||
assert np.all(mm.plot_components["limit_map"].image == 25.0)
|
||||
|
||||
mm.set_background_value(50)
|
||||
qtbot.wait(200)
|
||||
|
||||
assert mm.config.background_value == 50
|
||||
assert np.all(mm.plot_components["limit_map"].image == 50.0)
|
||||
|
||||
|
||||
def test_motor_map_init_from_config(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
config = {
|
||||
"widget_class": "BECMotorMap",
|
||||
"gui_id": "mm_id",
|
||||
"parent_id": bec_figure.gui_id,
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"axis": {
|
||||
"title": "Motor position: (-0.0, 0.0)",
|
||||
"title_size": None,
|
||||
"x_label": "Motor X (samx)",
|
||||
"x_label_size": None,
|
||||
"y_label": "Motor Y (samy)",
|
||||
"y_label_size": None,
|
||||
"legend_label_size": None,
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_lim": None,
|
||||
"y_lim": None,
|
||||
"x_grid": True,
|
||||
"y_grid": True,
|
||||
"outer_axes": False,
|
||||
},
|
||||
"signals": {
|
||||
"source": "device_readback",
|
||||
"x": {
|
||||
"name": "samx",
|
||||
"entry": "samx",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": [-10.0, 10.0],
|
||||
},
|
||||
"y": {
|
||||
"name": "samy",
|
||||
"entry": "samy",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": [-5.0, 5.0],
|
||||
},
|
||||
"z": None,
|
||||
"dap": None,
|
||||
},
|
||||
"color": (255, 255, 255, 255),
|
||||
"scatter_size": 5,
|
||||
"max_points": 50,
|
||||
"num_dim_points": 10,
|
||||
"precision": 5,
|
||||
"background_value": 50,
|
||||
}
|
||||
mm = bec_figure.motor_map(config=config)
|
||||
config["gui_id"] = mm.gui_id
|
||||
|
||||
assert mm._config_dict == config
|
||||
|
||||
|
||||
def test_motor_map_set_scatter_size(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert mm.config.scatter_size == 5
|
||||
assert mm.plot_components["scatter"].opts["size"] == 5
|
||||
|
||||
mm.set_scatter_size(10)
|
||||
qtbot.wait(200)
|
||||
|
||||
assert mm.config.scatter_size == 10
|
||||
assert mm.plot_components["scatter"].opts["size"] == 10
|
||||
|
||||
|
||||
def test_motor_map_change_precision(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert mm.config.precision == 2
|
||||
mm.set_precision(10)
|
||||
assert mm.config.precision == 10
|
||||
|
||||
|
||||
def test_motor_map_set_color(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert mm.config.color == (255, 255, 255, 255)
|
||||
|
||||
mm.set_color((0, 0, 0, 255))
|
||||
qtbot.wait(200)
|
||||
assert mm.config.color == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_motor_map_get_data_max_points(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
mm = bec_figure.motor_map("samx", "samy")
|
||||
motor_map_dev = mm.client.device_manager.devices
|
||||
|
||||
init_positions = {
|
||||
"samx": [motor_map_dev["samx"].read()["samx"]["value"]],
|
||||
"samy": [motor_map_dev["samy"].read()["samy"]["value"]],
|
||||
}
|
||||
msg = DeviceMessage(signals={"samx": {"value": 5.0}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
msg = DeviceMessage(signals={"samy": {"value": 9.0}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
msg = DeviceMessage(signals={"samx": {"value": 6.0}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
msg = DeviceMessage(signals={"samy": {"value": 7.0}}, metadata={})
|
||||
mm.on_device_readback(msg.content, msg.metadata)
|
||||
|
||||
expected_x = [init_positions["samx"][-1], 5.0, 5.0, 6.0, 6.0]
|
||||
expected_y = [init_positions["samy"][-1], init_positions["samy"][-1], 9.0, 9.0, 7.0]
|
||||
get_data = mm.get_data()
|
||||
|
||||
assert mm.database_buffer["x"] == expected_x
|
||||
assert mm.database_buffer["y"] == expected_y
|
||||
assert get_data["x"] == expected_x
|
||||
assert get_data["y"] == expected_y
|
||||
|
||||
mm.set_max_points(3)
|
||||
qtbot.wait(200)
|
||||
get_data = mm.get_data()
|
||||
assert len(get_data["x"]) == 3
|
||||
assert len(get_data["y"]) == 3
|
||||
assert get_data["x"] == expected_x[-3:]
|
||||
assert get_data["y"] == expected_y[-3:]
|
||||
assert mm.database_buffer["x"] == expected_x[-3:]
|
||||
assert mm.database_buffer["y"] == expected_y[-3:]
|
@ -1,253 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib.endpoints import messages
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_set_monitor(qtbot, mocked_client):
|
||||
"""Test that setting the monitor connects the appropriate slot."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("waveform1d")
|
||||
|
||||
assert multi_waveform.config.monitor == "waveform1d"
|
||||
assert multi_waveform.connected is True
|
||||
|
||||
data_0 = np.random.rand(100)
|
||||
msg = messages.DeviceMonitor1DMessage(
|
||||
device="waveform1d", data=data_0, metadata={"scan_id": "12345"}
|
||||
)
|
||||
multi_waveform.on_monitor_1d_update(msg.content, msg.metadata)
|
||||
data_waveform = multi_waveform.get_all_data()
|
||||
print(data_waveform)
|
||||
|
||||
assert len(data_waveform) == 1
|
||||
assert np.array_equal(data_waveform["curve_0"]["y"], data_0)
|
||||
|
||||
data_1 = np.random.rand(100)
|
||||
msg = messages.DeviceMonitor1DMessage(
|
||||
device="waveform1d", data=data_1, metadata={"scan_id": "12345"}
|
||||
)
|
||||
multi_waveform.on_monitor_1d_update(msg.content, msg.metadata)
|
||||
|
||||
data_waveform = multi_waveform.get_all_data()
|
||||
assert len(data_waveform) == 2
|
||||
assert np.array_equal(data_waveform["curve_0"]["y"], data_0)
|
||||
assert np.array_equal(data_waveform["curve_1"]["y"], data_1)
|
||||
|
||||
|
||||
def test_on_monitor_1d_update(qtbot, mocked_client):
|
||||
"""Test that data updates add curves to the plot."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("test_monitor")
|
||||
|
||||
# Simulate receiving data updates
|
||||
test_data = np.array([1, 2, 3, 4, 5])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
|
||||
# Call the on_monitor_1d_update method
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Check that a curve has been added
|
||||
assert len(multi_waveform.curves) == 1
|
||||
# Check that the data in the curve is correct
|
||||
curve = multi_waveform.curves[-1]
|
||||
x_data, y_data = curve.getData()
|
||||
assert np.array_equal(y_data, test_data)
|
||||
|
||||
# Simulate another data update
|
||||
test_data_2 = np.array([6, 7, 8, 9, 10])
|
||||
msg2 = {"data": test_data_2}
|
||||
metadata2 = {"scan_id": "scan_1"}
|
||||
|
||||
multi_waveform.on_monitor_1d_update(msg2, metadata2)
|
||||
|
||||
# Check that another curve has been added
|
||||
assert len(multi_waveform.curves) == 2
|
||||
# Check that the data in the curve is correct
|
||||
curve2 = multi_waveform.curves[-1]
|
||||
x_data2, y_data2 = curve2.getData()
|
||||
assert np.array_equal(y_data2, test_data_2)
|
||||
|
||||
|
||||
def test_set_curve_limit_no_flush(qtbot, mocked_client):
|
||||
"""Test set_curve_limit with flush_buffer=False."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("test_monitor")
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(5):
|
||||
test_data = np.array([i, i + 1, i + 2])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Check that there are 5 curves
|
||||
assert len(multi_waveform.curves) == 5
|
||||
# Set curve limit to 3 with flush_buffer=False
|
||||
multi_waveform.set_curve_limit(3, flush_buffer=False)
|
||||
|
||||
# Check that curves are hidden, but not removed
|
||||
assert len(multi_waveform.curves) == 5
|
||||
visible_curves = [curve for curve in multi_waveform.curves if curve.isVisible()]
|
||||
assert len(visible_curves) == 3
|
||||
# The first two curves should be hidden
|
||||
assert not multi_waveform.curves[0].isVisible()
|
||||
assert not multi_waveform.curves[1].isVisible()
|
||||
assert multi_waveform.curves[2].isVisible()
|
||||
assert multi_waveform.curves[3].isVisible()
|
||||
assert multi_waveform.curves[4].isVisible()
|
||||
|
||||
|
||||
def test_set_curve_limit_flush(qtbot, mocked_client):
|
||||
"""Test set_curve_limit with flush_buffer=True."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("test_monitor")
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(5):
|
||||
test_data = np.array([i, i + 1, i + 2])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Check that there are 5 curves
|
||||
assert len(multi_waveform.curves) == 5
|
||||
# Set curve limit to 3 with flush_buffer=True
|
||||
multi_waveform.set_curve_limit(3, flush_buffer=True)
|
||||
|
||||
# Check that only 3 curves remain
|
||||
assert len(multi_waveform.curves) == 3
|
||||
# The curves should be the last 3 added
|
||||
x_data, y_data = multi_waveform.curves[0].getData()
|
||||
assert np.array_equal(y_data, [2, 3, 4])
|
||||
x_data, y_data = multi_waveform.curves[1].getData()
|
||||
assert np.array_equal(y_data, [3, 4, 5])
|
||||
x_data, y_data = multi_waveform.curves[2].getData()
|
||||
assert np.array_equal(y_data, [4, 5, 6])
|
||||
|
||||
|
||||
def test_set_curve_highlight(qtbot, mocked_client):
|
||||
"""Test that the correct curve is highlighted."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("test_monitor")
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(3):
|
||||
test_data = np.array([i, i + 1, i + 2])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Set highlight_last_curve to False
|
||||
multi_waveform.highlight_last_curve = False
|
||||
multi_waveform.set_curve_highlight(1) # Highlight the second curve (index 1)
|
||||
|
||||
# Check that the second curve is highlighted
|
||||
visible_curves = [curve for curve in multi_waveform.curves if curve.isVisible()]
|
||||
# Reverse the list to match indexing in set_curve_highlight
|
||||
visible_curves = list(reversed(visible_curves))
|
||||
for i, curve in enumerate(visible_curves):
|
||||
pen = curve.opts["pen"]
|
||||
width = pen.width()
|
||||
if i == 1:
|
||||
# Highlighted curve should have width 5
|
||||
assert width == 5
|
||||
else:
|
||||
assert width == 1
|
||||
|
||||
|
||||
def test_set_opacity(qtbot, mocked_client):
|
||||
"""Test that setting opacity updates the curves."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("waveform1d")
|
||||
|
||||
# Simulate adding a curve
|
||||
test_data = np.array([1, 2, 3])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Set opacity to 30
|
||||
multi_waveform.set_opacity(30)
|
||||
assert multi_waveform.config.opacity == 30
|
||||
|
||||
|
||||
def test_set_colormap(qtbot, mocked_client):
|
||||
"""Test that setting the colormap updates the curve colors."""
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("waveform1d")
|
||||
|
||||
# Simulate adding multiple curves
|
||||
for i in range(3):
|
||||
test_data = np.array([i, i + 1, i + 2])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Set a new colormap
|
||||
multi_waveform.set_opacity(100)
|
||||
multi_waveform.set_colormap("viridis")
|
||||
# Check that the colors of the curves have changed accordingly
|
||||
visible_curves = [curve for curve in multi_waveform.curves if curve.isVisible()]
|
||||
# Get the colors applied
|
||||
colors = Colors.evenly_spaced_colors(colormap="viridis", num=len(visible_curves), format="HEX")
|
||||
for i, curve in enumerate(visible_curves):
|
||||
pen = curve.opts["pen"]
|
||||
pen_color = pen.color().name()
|
||||
expected_color = colors[i]
|
||||
# Compare pen color to expected color
|
||||
assert pen_color.lower() == expected_color.lower()
|
||||
|
||||
|
||||
def test_export_to_matplotlib(qtbot, mocked_client):
|
||||
"""Test that export_to_matplotlib can be called without errors."""
|
||||
try:
|
||||
import matplotlib
|
||||
except ImportError:
|
||||
pytest.skip("Matplotlib not installed")
|
||||
|
||||
# Create a BECFigure
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
# Add a multi_waveform plot
|
||||
multi_waveform = bec_figure.multi_waveform()
|
||||
multi_waveform.set_monitor("test_monitor")
|
||||
|
||||
# Simulate adding a curve
|
||||
test_data = np.array([1, 2, 3])
|
||||
msg = {"data": test_data}
|
||||
metadata = {"scan_id": "scan_1"}
|
||||
multi_waveform.on_monitor_1d_update(msg, metadata)
|
||||
|
||||
# Call export_to_matplotlib
|
||||
with mock.patch("pyqtgraph.exporters.MatplotlibExporter.export") as mock_export:
|
||||
multi_waveform.export_to_matplotlib()
|
||||
mock_export.assert_called_once()
|
@ -1,247 +0,0 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_init_plot_base(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
assert plot_base is not None
|
||||
assert plot_base.config.widget_class == "BECPlotBase"
|
||||
assert plot_base.config.gui_id == plot_base.gui_id
|
||||
|
||||
|
||||
def test_plot_base_axes_by_separate_methods(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
plot_base.set_title("Test Title")
|
||||
plot_base.set_x_label("Test x Label")
|
||||
plot_base.set_y_label("Test y Label")
|
||||
plot_base.set_x_lim(1, 100)
|
||||
plot_base.set_y_lim(5, 500)
|
||||
plot_base.set_grid(True, True)
|
||||
plot_base.set_x_scale("log")
|
||||
plot_base.set_y_scale("log")
|
||||
|
||||
assert plot_base.plot_item.titleLabel.text == "Test Title"
|
||||
assert plot_base.config.axis.title == "Test Title"
|
||||
assert plot_base.plot_item.getAxis("bottom").labelText == "Test x Label"
|
||||
assert plot_base.config.axis.x_label == "Test x Label"
|
||||
assert plot_base.plot_item.getAxis("left").labelText == "Test y Label"
|
||||
assert plot_base.config.axis.y_label == "Test y Label"
|
||||
assert plot_base.config.axis.x_lim == (1, 100)
|
||||
assert plot_base.config.axis.y_lim == (5, 500)
|
||||
assert plot_base.plot_item.ctrl.xGridCheck.isChecked() == True
|
||||
assert plot_base.plot_item.ctrl.yGridCheck.isChecked() == True
|
||||
assert plot_base.plot_item.ctrl.logXCheck.isChecked() == True
|
||||
assert plot_base.plot_item.ctrl.logYCheck.isChecked() == True
|
||||
|
||||
# Check the font size by mocking the set functions
|
||||
# I struggled retrieving it from the QFont object directly
|
||||
# thus I mocked the set functions to check internally the functionality
|
||||
with (
|
||||
mock.patch.object(plot_base.plot_item, "setLabel") as mock_set_label,
|
||||
mock.patch.object(plot_base.plot_item, "setTitle") as mock_set_title,
|
||||
):
|
||||
plot_base.set_x_label("Test x Label", 20)
|
||||
plot_base.set_y_label("Test y Label", 16)
|
||||
assert mock_set_label.call_count == 2
|
||||
assert plot_base.config.axis.x_label_size == 20
|
||||
assert plot_base.config.axis.y_label_size == 16
|
||||
col = plot_base.get_text_color()
|
||||
calls = []
|
||||
style = {"color": col, "font-size": "20pt"}
|
||||
calls.append(mock.call("bottom", "Test x Label", **style))
|
||||
style = {"color": col, "font-size": "16pt"}
|
||||
calls.append(mock.call("left", "Test y Label", **style))
|
||||
assert mock_set_label.call_args_list == calls
|
||||
plot_base.set_title("Test Title", 16)
|
||||
style = {"color": col, "size": "16pt"}
|
||||
call = mock.call("Test Title", **style)
|
||||
assert mock_set_title.call_args == call
|
||||
|
||||
|
||||
def test_plot_base_axes_added_by_kwargs(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
plot_base.set(
|
||||
title="Test Title",
|
||||
x_label="Test x Label",
|
||||
y_label="Test y Label",
|
||||
x_lim=(1, 100),
|
||||
y_lim=(5, 500),
|
||||
x_scale="log",
|
||||
y_scale="log",
|
||||
)
|
||||
|
||||
assert plot_base.plot_item.titleLabel.text == "Test Title"
|
||||
assert plot_base.config.axis.title == "Test Title"
|
||||
assert plot_base.plot_item.getAxis("bottom").labelText == "Test x Label"
|
||||
assert plot_base.config.axis.x_label == "Test x Label"
|
||||
assert plot_base.plot_item.getAxis("left").labelText == "Test y Label"
|
||||
assert plot_base.config.axis.y_label == "Test y Label"
|
||||
assert plot_base.config.axis.x_lim == (1, 100)
|
||||
assert plot_base.config.axis.y_lim == (5, 500)
|
||||
assert plot_base.plot_item.ctrl.logXCheck.isChecked() == True
|
||||
assert plot_base.plot_item.ctrl.logYCheck.isChecked() == True
|
||||
|
||||
|
||||
def test_lock_aspect_ratio(qtbot, mocked_client):
|
||||
"""
|
||||
Test locking and unlocking the aspect ratio of the plot.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
# Lock the aspect ratio
|
||||
plot_base.lock_aspect_ratio(True)
|
||||
assert plot_base.plot_item.vb.state["aspectLocked"] == 1
|
||||
|
||||
# Unlock the aspect ratio
|
||||
plot_base.lock_aspect_ratio(False)
|
||||
assert plot_base.plot_item.vb.state["aspectLocked"] == 0
|
||||
|
||||
|
||||
def test_set_auto_range(qtbot, mocked_client):
|
||||
"""
|
||||
Test enabling and disabling auto range for the plot.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
# Enable auto range for both axes
|
||||
plot_base.set_auto_range(True, axis="xy")
|
||||
assert plot_base.plot_item.vb.state["autoRange"] == [True, True]
|
||||
|
||||
# Disable auto range for x-axis
|
||||
plot_base.set_auto_range(False, axis="x")
|
||||
assert plot_base.plot_item.vb.state["autoRange"] == [False, True]
|
||||
|
||||
# Disable auto range for y-axis
|
||||
plot_base.set_auto_range(False, axis="y")
|
||||
assert plot_base.plot_item.vb.state["autoRange"] == [False, False]
|
||||
|
||||
|
||||
def test_set_outer_axes(qtbot, mocked_client):
|
||||
"""
|
||||
Test showing and hiding the outer axes of the plot.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
# Show outer axes
|
||||
plot_base.set_outer_axes(True)
|
||||
assert plot_base.plot_item.getAxis("top").isVisible()
|
||||
assert plot_base.plot_item.getAxis("right").isVisible()
|
||||
assert plot_base.config.axis.outer_axes is True
|
||||
|
||||
# Hide outer axes
|
||||
plot_base.set_outer_axes(False)
|
||||
assert not plot_base.plot_item.getAxis("top").isVisible()
|
||||
assert not plot_base.plot_item.getAxis("right").isVisible()
|
||||
assert plot_base.config.axis.outer_axes is False
|
||||
|
||||
|
||||
def test_toggle_crosshair(qtbot, mocked_client):
|
||||
"""
|
||||
Test toggling the crosshair on and off.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
# Toggle crosshair on
|
||||
plot_base.toggle_crosshair()
|
||||
assert plot_base.crosshair is not None
|
||||
|
||||
# Toggle crosshair off
|
||||
plot_base.toggle_crosshair()
|
||||
assert plot_base.crosshair is None
|
||||
|
||||
|
||||
def test_invalid_scale_input(qtbot, mocked_client):
|
||||
"""
|
||||
Test setting an invalid scale for x and y axes.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_x_scale("invalid_scale")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_y_scale("invalid_scale")
|
||||
|
||||
|
||||
def test_set_x_lim_invalid_arguments(qtbot, mocked_client):
|
||||
"""
|
||||
Test passing invalid arguments to set_x_lim.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_x_lim(1)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_x_lim((1, 2, 3))
|
||||
|
||||
|
||||
def test_set_y_lim_invalid_arguments(qtbot, mocked_client):
|
||||
"""
|
||||
Test passing invalid arguments to set_y_lim.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_y_lim(1)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
plot_base.set_y_lim((1, 2, 3))
|
||||
|
||||
|
||||
def test_remove_plot(qtbot, mocked_client):
|
||||
"""
|
||||
Test removing the plot widget from the figure.
|
||||
"""
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
with mock.patch.object(bec_figure, "remove") as mock_remove:
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
plot_base.remove()
|
||||
mock_remove.assert_called_once_with(widget_id=plot_base.gui_id)
|
||||
|
||||
|
||||
def test_add_fps_monitor(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
plot_base.enable_fps_monitor(True)
|
||||
|
||||
assert plot_base.fps_monitor is not None
|
||||
assert plot_base.fps_monitor.view_box is plot_base.plot_item.getViewBox()
|
||||
assert plot_base.fps_monitor.timer.isActive() == True
|
||||
assert plot_base.fps_monitor.timer.interval() == 1000
|
||||
assert plot_base.fps_monitor.sigFpsUpdate is not None
|
||||
assert plot_base.fps_monitor.sigFpsUpdate.connect is not None
|
||||
|
||||
|
||||
def test_hook_unhook_fps_monitor(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot_base = bec_figure.add_widget(widget_type="BECPlotBase", widget_id="test_plot")
|
||||
|
||||
plot_base.enable_fps_monitor(True)
|
||||
assert plot_base.fps_monitor is not None
|
||||
|
||||
plot_base.enable_fps_monitor(False)
|
||||
assert plot_base.fps_monitor is None
|
||||
|
||||
plot_base.enable_fps_monitor(True)
|
||||
assert plot_base.fps_monitor is not None
|
@ -6,7 +6,7 @@ def test_client_generator_classes():
|
||||
connector_cls_names = [cls.__name__ for cls in out.connector_classes]
|
||||
plugins = [cls.__name__ for cls in out.plugins]
|
||||
|
||||
assert "BECFigure" in connector_cls_names
|
||||
assert "BECWaveform" in connector_cls_names
|
||||
assert "Image" in connector_cls_names
|
||||
assert "Waveform" in connector_cls_names
|
||||
assert "BECDockArea" in plugins
|
||||
assert "BECWaveform" not in plugins
|
||||
assert "NonExisting" not in plugins
|
||||
|
@ -3,7 +3,7 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from bec_widgets.cli.server import _start_server
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -20,9 +20,9 @@ def test_rpc_server_start_server_without_service_config(mocked_cli_server):
|
||||
"""
|
||||
mock_server, mock_config, _ = mocked_cli_server
|
||||
|
||||
_start_server("gui_id", BECFigure, config=None)
|
||||
_start_server("gui_id", BECDockArea, config=None)
|
||||
mock_server.assert_called_once_with(
|
||||
gui_id="gui_id", config=mock_config(), gui_class=BECFigure, gui_class_id="bec"
|
||||
gui_id="gui_id", config=mock_config(), gui_class=BECDockArea, gui_class_id="bec"
|
||||
)
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ def test_rpc_server_start_server_with_service_config(mocked_cli_server, config,
|
||||
"""
|
||||
mock_server, mock_config, _ = mocked_cli_server
|
||||
config = mock_config(**call_config)
|
||||
_start_server("gui_id", BECFigure, config=config)
|
||||
_start_server("gui_id", BECDockArea, config=config)
|
||||
mock_server.assert_called_once_with(
|
||||
gui_id="gui_id", config=config, gui_class=BECFigure, gui_class_id="bec"
|
||||
gui_id="gui_id", config=config, gui_class=BECDockArea, gui_class_id="bec"
|
||||
)
|
||||
|
@ -1,113 +1,114 @@
|
||||
import pytest
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plot_widget_with_arrow_item(qtbot, mocked_client):
|
||||
widget = BECFigure(client=mocked_client())
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
waveform = widget.plot()
|
||||
|
||||
yield waveform.arrow_item, waveform.plot_item
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plot_widget_with_tick_item(qtbot, mocked_client):
|
||||
widget = BECFigure(client=mocked_client())
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
waveform = widget.plot()
|
||||
|
||||
yield waveform.tick_item, waveform.plot_item
|
||||
|
||||
|
||||
def test_arrow_item_add_to_plot(plot_widget_with_arrow_item):
|
||||
"""Test the add_to_plot method"""
|
||||
arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
assert arrow_item.plot_item is not None
|
||||
assert arrow_item.plot_item.items == []
|
||||
arrow_item.add_to_plot()
|
||||
assert arrow_item.plot_item.items == [arrow_item.arrow_item]
|
||||
arrow_item.remove_from_plot()
|
||||
|
||||
|
||||
def test_arrow_item_set_position(plot_widget_with_arrow_item):
|
||||
"""Test the set_position method"""
|
||||
arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
container = []
|
||||
|
||||
def signal_callback(tup: tuple):
|
||||
container.append(tup)
|
||||
|
||||
arrow_item.add_to_plot()
|
||||
arrow_item.position_changed.connect(signal_callback)
|
||||
arrow_item.set_position(pos=(1, 1))
|
||||
point = QPointF(1.0, 1.0)
|
||||
assert arrow_item.arrow_item.pos() == point
|
||||
arrow_item.set_position(pos=(2, 2))
|
||||
point = QPointF(2.0, 2.0)
|
||||
assert arrow_item.arrow_item.pos() == point
|
||||
assert container == [(1, 1), (2, 2)]
|
||||
arrow_item.remove_from_plot()
|
||||
|
||||
|
||||
def test_arrow_item_cleanup(plot_widget_with_arrow_item):
|
||||
"""Test cleanup procedure"""
|
||||
arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
arrow_item.add_to_plot()
|
||||
assert arrow_item.item_on_plot is True
|
||||
arrow_item.cleanup()
|
||||
assert arrow_item.plot_item.items == []
|
||||
assert arrow_item.item_on_plot is False
|
||||
assert arrow_item.arrow_item is None
|
||||
|
||||
|
||||
def test_tick_item_add_to_plot(plot_widget_with_tick_item):
|
||||
"""Test the add_to_plot method"""
|
||||
tick_item, plot_item = plot_widget_with_tick_item
|
||||
assert tick_item.plot_item is not None
|
||||
assert tick_item.plot_item.items == []
|
||||
tick_item.add_to_plot()
|
||||
assert tick_item.plot_item.layout.itemAt(2, 1) == tick_item.tick_item
|
||||
assert tick_item.item_on_plot is True
|
||||
new_pos = plot_item.vb.geometry().bottom()
|
||||
pos = tick_item.tick.pos()
|
||||
new_pos = tick_item.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
|
||||
assert new_pos.y() == pos.y()
|
||||
tick_item.remove_from_plot()
|
||||
|
||||
|
||||
def test_tick_item_set_position(plot_widget_with_tick_item):
|
||||
"""Test the set_position method"""
|
||||
tick_item, plot_item = plot_widget_with_tick_item
|
||||
container = []
|
||||
|
||||
def signal_callback(val: float):
|
||||
container.append(val)
|
||||
|
||||
tick_item.add_to_plot()
|
||||
tick_item.position_changed.connect(signal_callback)
|
||||
|
||||
tick_item.set_position(pos=1)
|
||||
assert tick_item._pos == 1
|
||||
tick_item.set_position(pos=2)
|
||||
assert tick_item._pos == 2
|
||||
assert container == [1.0, 2.0]
|
||||
tick_item.remove_from_plot()
|
||||
|
||||
|
||||
def test_tick_item_cleanup(plot_widget_with_tick_item):
|
||||
"""Test cleanup procedure"""
|
||||
tick_item, plot_item = plot_widget_with_tick_item
|
||||
tick_item.add_to_plot()
|
||||
assert tick_item.item_on_plot is True
|
||||
tick_item.cleanup()
|
||||
ticks = getattr(tick_item.plot_item.layout.itemAt(3, 1), "ticks", None)
|
||||
assert ticks == None
|
||||
assert tick_item.item_on_plot is False
|
||||
assert tick_item.tick_item is None
|
||||
# TODO temporary disabled until migrate tick and arrow items to new system
|
||||
# import pytest
|
||||
# from qtpy.QtCore import QPointF
|
||||
#
|
||||
# from bec_widgets.widgets.containers.figure import BECFigure
|
||||
#
|
||||
# from .client_mocks import mocked_client
|
||||
#
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def plot_widget_with_arrow_item(qtbot, mocked_client):
|
||||
# widget = BECFigure(client=mocked_client())
|
||||
# qtbot.addWidget(widget)
|
||||
# qtbot.waitExposed(widget)
|
||||
# waveform = widget.plot()
|
||||
#
|
||||
# yield waveform.arrow_item, waveform.plot_item
|
||||
#
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def plot_widget_with_tick_item(qtbot, mocked_client):
|
||||
# widget = BECFigure(client=mocked_client())
|
||||
# qtbot.addWidget(widget)
|
||||
# qtbot.waitExposed(widget)
|
||||
# waveform = widget.plot()
|
||||
#
|
||||
# yield waveform.tick_item, waveform.plot_item
|
||||
#
|
||||
#
|
||||
# def test_arrow_item_add_to_plot(plot_widget_with_arrow_item):
|
||||
# """Test the add_to_plot method"""
|
||||
# arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
# assert arrow_item.plot_item is not None
|
||||
# assert arrow_item.plot_item.items == []
|
||||
# arrow_item.add_to_plot()
|
||||
# assert arrow_item.plot_item.items == [arrow_item.arrow_item]
|
||||
# arrow_item.remove_from_plot()
|
||||
#
|
||||
#
|
||||
# def test_arrow_item_set_position(plot_widget_with_arrow_item):
|
||||
# """Test the set_position method"""
|
||||
# arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
# container = []
|
||||
#
|
||||
# def signal_callback(tup: tuple):
|
||||
# container.append(tup)
|
||||
#
|
||||
# arrow_item.add_to_plot()
|
||||
# arrow_item.position_changed.connect(signal_callback)
|
||||
# arrow_item.set_position(pos=(1, 1))
|
||||
# point = QPointF(1.0, 1.0)
|
||||
# assert arrow_item.arrow_item.pos() == point
|
||||
# arrow_item.set_position(pos=(2, 2))
|
||||
# point = QPointF(2.0, 2.0)
|
||||
# assert arrow_item.arrow_item.pos() == point
|
||||
# assert container == [(1, 1), (2, 2)]
|
||||
# arrow_item.remove_from_plot()
|
||||
#
|
||||
#
|
||||
# def test_arrow_item_cleanup(plot_widget_with_arrow_item):
|
||||
# """Test cleanup procedure"""
|
||||
# arrow_item, plot_item = plot_widget_with_arrow_item
|
||||
# arrow_item.add_to_plot()
|
||||
# assert arrow_item.item_on_plot is True
|
||||
# arrow_item.cleanup()
|
||||
# assert arrow_item.plot_item.items == []
|
||||
# assert arrow_item.item_on_plot is False
|
||||
# assert arrow_item.arrow_item is None
|
||||
#
|
||||
#
|
||||
# def test_tick_item_add_to_plot(plot_widget_with_tick_item):
|
||||
# """Test the add_to_plot method"""
|
||||
# tick_item, plot_item = plot_widget_with_tick_item
|
||||
# assert tick_item.plot_item is not None
|
||||
# assert tick_item.plot_item.items == []
|
||||
# tick_item.add_to_plot()
|
||||
# assert tick_item.plot_item.layout.itemAt(2, 1) == tick_item.tick_item
|
||||
# assert tick_item.item_on_plot is True
|
||||
# new_pos = plot_item.vb.geometry().bottom()
|
||||
# pos = tick_item.tick.pos()
|
||||
# new_pos = tick_item.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
|
||||
# assert new_pos.y() == pos.y()
|
||||
# tick_item.remove_from_plot()
|
||||
#
|
||||
#
|
||||
# def test_tick_item_set_position(plot_widget_with_tick_item):
|
||||
# """Test the set_position method"""
|
||||
# tick_item, plot_item = plot_widget_with_tick_item
|
||||
# container = []
|
||||
#
|
||||
# def signal_callback(val: float):
|
||||
# container.append(val)
|
||||
#
|
||||
# tick_item.add_to_plot()
|
||||
# tick_item.position_changed.connect(signal_callback)
|
||||
#
|
||||
# tick_item.set_position(pos=1)
|
||||
# assert tick_item._pos == 1
|
||||
# tick_item.set_position(pos=2)
|
||||
# assert tick_item._pos == 2
|
||||
# assert container == [1.0, 2.0]
|
||||
# tick_item.remove_from_plot()
|
||||
#
|
||||
#
|
||||
# def test_tick_item_cleanup(plot_widget_with_tick_item):
|
||||
# """Test cleanup procedure"""
|
||||
# tick_item, plot_item = plot_widget_with_tick_item
|
||||
# tick_item.add_to_plot()
|
||||
# assert tick_item.item_on_plot is True
|
||||
# tick_item.cleanup()
|
||||
# ticks = getattr(tick_item.plot_item.layout.itemAt(3, 1), "ticks", None)
|
||||
# assert ticks == None
|
||||
# assert tick_item.item_on_plot is False
|
||||
# assert tick_item.tick_item is None
|
||||
|
@ -1,766 +0,0 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib.scan_items import ScanItem
|
||||
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.figure.plots.waveform.waveform_curve import (
|
||||
CurveConfig,
|
||||
Signal,
|
||||
SignalData,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_adding_curve_to_waveform(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
# adding curve which is in bec - only names
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
assert c1.config.label == "bpm4i-bpm4i"
|
||||
|
||||
# adding curve which is in bec - names and entry
|
||||
c2 = w1.add_curve_bec(x_name="samx", x_entry="samx", y_name="bpm3a", y_entry="bpm3a")
|
||||
assert c2.config.label == "bpm3a-bpm3a"
|
||||
|
||||
# adding curve which is not in bec
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.add_curve_bec(x_name="non_existent_device", y_name="non_existent_device")
|
||||
assert "Device 'non_existent_device' not found in current BEC session" in str(excinfo.value)
|
||||
|
||||
# adding wrong entry for samx
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.add_curve_bec(
|
||||
x_name="samx", x_entry="non_existent_entry", y_name="bpm3a", y_entry="bpm3a"
|
||||
)
|
||||
assert "Entry 'non_existent_entry' not found in device 'samx' signals" in str(excinfo.value)
|
||||
|
||||
# adding wrong device with validation switched off
|
||||
c3 = w1.add_curve_bec(x_name="samx", y_name="non_existent_device", validate_bec=False)
|
||||
assert c3.config.label == "non_existent_device-non_existent_device"
|
||||
|
||||
|
||||
def test_adding_curve_with_same_id(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
|
||||
assert "Curve with ID 'test_curve' already exists." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_create_waveform1D_by_config(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1_config_input = {
|
||||
"widget_class": "BECWaveform",
|
||||
"gui_id": "widget_1",
|
||||
"parent_id": "BECFigure_1708689320.788527",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"axis": {
|
||||
"title": "Widget 1",
|
||||
"title_size": None,
|
||||
"x_label": None,
|
||||
"x_label_size": None,
|
||||
"y_label": None,
|
||||
"y_label_size": None,
|
||||
"legend_label_size": None,
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_lim": (1, 10),
|
||||
"y_lim": None,
|
||||
"x_grid": False,
|
||||
"y_grid": False,
|
||||
"outer_axes": False,
|
||||
},
|
||||
"color_palette": "magma",
|
||||
"curves": {
|
||||
"bpm4i-bpm4i": {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.226847",
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 4,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
"dap": None,
|
||||
"source": "scan_segment",
|
||||
"x": {
|
||||
"name": "samx",
|
||||
"entry": "samx",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": None,
|
||||
},
|
||||
"y": {
|
||||
"name": "bpm4i",
|
||||
"entry": "bpm4i",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": None,
|
||||
},
|
||||
"z": None,
|
||||
},
|
||||
},
|
||||
"curve-custom": {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.22867",
|
||||
"parent_id": "widget_1",
|
||||
"label": "curve-custom",
|
||||
"color": "blue",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 5,
|
||||
"pen_style": "dashdot",
|
||||
"source": "custom",
|
||||
"signals": None,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w1 = bec_figure.plot(config=w1_config_input)
|
||||
|
||||
w1_config_output = w1.get_config()
|
||||
w1_config_input["gui_id"] = w1.gui_id
|
||||
|
||||
assert w1_config_input == w1_config_output
|
||||
assert w1.plot_item.titleLabel.text == "Widget 1"
|
||||
assert w1.config.axis.title == "Widget 1"
|
||||
|
||||
|
||||
def test_change_gui_id(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
w1.change_gui_id("new_id")
|
||||
|
||||
assert w1.config.gui_id == "new_id"
|
||||
assert c1.config.parent_id == "new_id"
|
||||
|
||||
|
||||
def test_getting_curve(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
|
||||
c1_expected_config_dark = CurveConfig(
|
||||
widget_class="BECCurve",
|
||||
gui_id="test_curve",
|
||||
parent_id=w1.gui_id,
|
||||
label="bpm4i-bpm4i",
|
||||
color="#3b0f70",
|
||||
symbol="o",
|
||||
symbol_color=None,
|
||||
symbol_size=7,
|
||||
pen_width=4,
|
||||
pen_style="solid",
|
||||
source="scan_segment",
|
||||
signals=Signal(
|
||||
source="scan_segment",
|
||||
x=SignalData(name="samx", entry="samx", unit=None, modifier=None),
|
||||
y=SignalData(name="bpm4i", entry="bpm4i", unit=None, modifier=None),
|
||||
),
|
||||
)
|
||||
c1_expected_config_light = CurveConfig(
|
||||
widget_class="BECCurve",
|
||||
gui_id="test_curve",
|
||||
parent_id=w1.gui_id,
|
||||
label="bpm4i-bpm4i",
|
||||
color="#000004",
|
||||
symbol="o",
|
||||
symbol_color=None,
|
||||
symbol_size=7,
|
||||
pen_width=4,
|
||||
pen_style="solid",
|
||||
source="scan_segment",
|
||||
signals=Signal(
|
||||
source="scan_segment",
|
||||
x=SignalData(name="samx", entry="samx", unit=None, modifier=None),
|
||||
y=SignalData(name="bpm4i", entry="bpm4i", unit=None, modifier=None),
|
||||
),
|
||||
)
|
||||
|
||||
assert (
|
||||
w1.curves[0].config == c1_expected_config_dark
|
||||
or w1.curves[0].config == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
w1._curves_data["scan_segment"]["bpm4i-bpm4i"].config == c1_expected_config_dark
|
||||
or w1._curves_data["scan_segment"]["bpm4i-bpm4i"].config == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
w1.get_curve(0).config == c1_expected_config_dark
|
||||
or w1.get_curve(0).config == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
w1.get_curve_config("bpm4i-bpm4i", dict_output=True) == c1_expected_config_dark.model_dump()
|
||||
or w1.get_curve_config("bpm4i-bpm4i", dict_output=True)
|
||||
== c1_expected_config_light.model_dump()
|
||||
)
|
||||
assert (
|
||||
w1.get_curve_config("bpm4i-bpm4i", dict_output=False) == c1_expected_config_dark
|
||||
or w1.get_curve_config("bpm4i-bpm4i", dict_output=False) == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
w1.get_curve("bpm4i-bpm4i").config == c1_expected_config_dark
|
||||
or w1.get_curve("bpm4i-bpm4i").config == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
c1.get_config(False) == c1_expected_config_dark
|
||||
or c1.get_config(False) == c1_expected_config_light
|
||||
)
|
||||
assert (
|
||||
c1.get_config() == c1_expected_config_dark.model_dump()
|
||||
or c1.get_config() == c1_expected_config_light.model_dump()
|
||||
)
|
||||
|
||||
|
||||
def test_getting_curve_errors(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i", gui_id="test_curve")
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.get_curve("non_existent_curve")
|
||||
assert "Curve with ID 'non_existent_curve' not found." in str(excinfo.value)
|
||||
with pytest.raises(IndexError) as excinfo:
|
||||
w1.get_curve(1)
|
||||
assert "list index out of range" in str(excinfo.value)
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.get_curve(1.2)
|
||||
assert "Identifier must be either an integer (index) or a string (curve_id)." in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
def test_add_curve(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
|
||||
assert len(w1.curves) == 1
|
||||
assert w1._curves_data["scan_segment"] == {"bpm4i-bpm4i": c1}
|
||||
assert c1.config.label == "bpm4i-bpm4i"
|
||||
assert c1.config.source == "scan_segment"
|
||||
|
||||
|
||||
def test_change_legend_font_size(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
plot = bec_figure.plot()
|
||||
|
||||
w1 = plot.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
my_func = plot.plot_item.legend
|
||||
with mock.patch.object(my_func, "setScale") as mock_set_scale:
|
||||
plot.set_legend_label_size(18)
|
||||
assert plot.config.axis.legend_label_size == 18
|
||||
assert mock_set_scale.call_count == 1
|
||||
assert mock_set_scale.call_args == mock.call(2)
|
||||
|
||||
|
||||
def test_remove_curve(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
w1.add_curve_bec(x_name="samx", y_name="bpm3a")
|
||||
w1.remove_curve(0)
|
||||
w1.remove_curve("bpm3a-bpm3a")
|
||||
|
||||
assert len(w1.plot_item.curves) == 0
|
||||
assert w1._curves_data["scan_segment"] == {}
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.remove_curve(1.2)
|
||||
assert "Each identifier must be either an integer (index) or a string (curve_id)." in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
def test_change_curve_appearance_methods(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
|
||||
c1.set_color("#0000ff")
|
||||
c1.set_symbol("x")
|
||||
c1.set_symbol_color("#ff0000")
|
||||
c1.set_symbol_size(10)
|
||||
c1.set_pen_width(3)
|
||||
c1.set_pen_style("dashdot")
|
||||
|
||||
qtbot.wait(500)
|
||||
assert c1.config.color == "#0000ff"
|
||||
assert c1.config.symbol == "x"
|
||||
assert c1.config.symbol_color == "#ff0000"
|
||||
assert c1.config.symbol_size == 10
|
||||
assert c1.config.pen_width == 3
|
||||
assert c1.config.pen_style == "dashdot"
|
||||
assert c1.config.source == "scan_segment"
|
||||
assert c1.config.signals.model_dump() == {
|
||||
"dap": None,
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
"z": None,
|
||||
}
|
||||
|
||||
|
||||
def test_change_curve_appearance_args(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
|
||||
c1.set(
|
||||
color="#0000ff",
|
||||
symbol="x",
|
||||
symbol_color="#ff0000",
|
||||
symbol_size=10,
|
||||
pen_width=3,
|
||||
pen_style="dashdot",
|
||||
)
|
||||
|
||||
assert c1.config.color == "#0000ff"
|
||||
assert c1.config.symbol == "x"
|
||||
assert c1.config.symbol_color == "#ff0000"
|
||||
assert c1.config.symbol_size == 10
|
||||
assert c1.config.pen_width == 3
|
||||
assert c1.config.pen_style == "dashdot"
|
||||
assert c1.config.source == "scan_segment"
|
||||
assert c1.config.signals.model_dump() == {
|
||||
"dap": None,
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
"z": None,
|
||||
}
|
||||
|
||||
|
||||
def test_set_custom_curve_data(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_custom(
|
||||
x=[1, 2, 3],
|
||||
y=[4, 5, 6],
|
||||
label="custom_curve",
|
||||
color="#0000ff",
|
||||
symbol="x",
|
||||
symbol_color="#ff0000",
|
||||
symbol_size=10,
|
||||
pen_width=3,
|
||||
pen_style="dashdot",
|
||||
)
|
||||
|
||||
x_init, y_init = c1.get_data()
|
||||
|
||||
assert np.array_equal(x_init, [1, 2, 3])
|
||||
assert np.array_equal(y_init, [4, 5, 6])
|
||||
assert c1.config.label == "custom_curve"
|
||||
assert c1.config.color == "#0000ff"
|
||||
assert c1.config.symbol == "x"
|
||||
assert c1.config.symbol_color == "#ff0000"
|
||||
assert c1.config.symbol_size == 10
|
||||
assert c1.config.pen_width == 3
|
||||
assert c1.config.pen_style == "dashdot"
|
||||
assert c1.config.source == "custom"
|
||||
assert c1.config.signals == None
|
||||
|
||||
c1.set_data(x=[4, 5, 6], y=[7, 8, 9])
|
||||
|
||||
x_new, y_new = c1.get_data()
|
||||
assert np.array_equal(x_new, [4, 5, 6])
|
||||
assert np.array_equal(y_new, [7, 8, 9])
|
||||
|
||||
|
||||
def test_custom_data_2D_array(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
|
||||
data = np.random.rand(10, 2)
|
||||
|
||||
plt = bec_figure.plot(data)
|
||||
|
||||
x, y = plt.curves[0].get_data()
|
||||
|
||||
assert np.array_equal(x, data[:, 0])
|
||||
assert np.array_equal(y, data[:, 1])
|
||||
|
||||
|
||||
def test_get_all_data(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_custom(
|
||||
x=[1, 2, 3],
|
||||
y=[4, 5, 6],
|
||||
label="custom_curve-1",
|
||||
color="#0000ff",
|
||||
symbol="x",
|
||||
symbol_color="#ff0000",
|
||||
symbol_size=10,
|
||||
pen_width=3,
|
||||
pen_style="dashdot",
|
||||
)
|
||||
|
||||
c2 = w1.add_curve_custom(
|
||||
x=[4, 5, 6],
|
||||
y=[7, 8, 9],
|
||||
label="custom_curve-2",
|
||||
color="#00ff00",
|
||||
symbol="o",
|
||||
symbol_color="#00ff00",
|
||||
symbol_size=20,
|
||||
pen_width=4,
|
||||
pen_style="dash",
|
||||
)
|
||||
|
||||
all_data = w1.get_all_data()
|
||||
|
||||
assert all_data == {
|
||||
"custom_curve-1": {"x": [1, 2, 3], "y": [4, 5, 6]},
|
||||
"custom_curve-2": {"x": [4, 5, 6], "y": [7, 8, 9]},
|
||||
}
|
||||
|
||||
|
||||
def test_curve_add_by_config(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1_config_input = {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.226847",
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 4,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
"dap": None,
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {
|
||||
"name": "bpm4i",
|
||||
"entry": "bpm4i",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": None,
|
||||
},
|
||||
"z": None,
|
||||
},
|
||||
}
|
||||
|
||||
c1 = w1.add_curve_by_config(c1_config_input)
|
||||
|
||||
c1_config_dict = c1.get_config()
|
||||
|
||||
assert c1_config_dict == c1_config_input
|
||||
assert c1.config == CurveConfig(**c1_config_input)
|
||||
assert c1.get_config(False) == CurveConfig(**c1_config_input)
|
||||
|
||||
|
||||
def test_scan_update(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="bpm4i")
|
||||
|
||||
msg_waveform = {
|
||||
"data": {
|
||||
"samx": {"samx": {"value": 10}},
|
||||
"bpm4i": {"bpm4i": {"value": 5}},
|
||||
"gauss_bpm": {"gauss_bpm": {"value": 6}},
|
||||
"gauss_adc1": {"gauss_adc1": {"value": 8}},
|
||||
"gauss_adc2": {"gauss_adc2": {"value": 9}},
|
||||
},
|
||||
"scan_id": 1,
|
||||
}
|
||||
# Mock scan_storage.find_scan_by_ID
|
||||
mock_scan_data_waveform = mock.MagicMock(spec=ScanItem)
|
||||
mock_scan_data_waveform.live_data = {
|
||||
device_name: {
|
||||
entry: mock.MagicMock(val=[msg_waveform["data"][device_name][entry]["value"]])
|
||||
for entry in msg_waveform["data"][device_name]
|
||||
}
|
||||
for device_name in msg_waveform["data"]
|
||||
}
|
||||
|
||||
metadata_waveform = {"scan_name": "line_scan"}
|
||||
|
||||
w1.queue.scan_storage.find_scan_by_ID.return_value = mock_scan_data_waveform
|
||||
|
||||
w1.on_scan_segment(msg_waveform, metadata_waveform)
|
||||
qtbot.wait(500)
|
||||
assert c1.get_data() == ([10], [5])
|
||||
|
||||
|
||||
def test_scan_history_with_val_access(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
w1.plot(x_name="samx", y_name="bpm4i")
|
||||
|
||||
mock_scan_data = {
|
||||
"samx": {"samx": mock.MagicMock(val=np.array([1, 2, 3]))}, # Use mock.MagicMock for .val
|
||||
"bpm4i": {"bpm4i": mock.MagicMock(val=np.array([4, 5, 6]))}, # Use mock.MagicMock for .val
|
||||
}
|
||||
|
||||
mock_scan_storage = mock.MagicMock()
|
||||
scan_item_mock = mock.MagicMock(spec=ScanItem)
|
||||
scan_item_mock.data = mock_scan_data
|
||||
mock_scan_storage.find_scan_by_ID.return_value = scan_item_mock
|
||||
w1.queue.scan_storage = mock_scan_storage
|
||||
|
||||
fake_scan_id = "fake_scan_id"
|
||||
w1.scan_history(scan_id=fake_scan_id)
|
||||
|
||||
qtbot.wait(500)
|
||||
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
|
||||
assert np.array_equal(x_data, [1, 2, 3])
|
||||
assert np.array_equal(y_data, [4, 5, 6])
|
||||
|
||||
|
||||
def test_scatter_2d_update(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
c1 = w1.add_curve_bec(x_name="samx", y_name="samx", z_name="bpm4i")
|
||||
|
||||
msg = {
|
||||
"data": {
|
||||
"samx": {"samx": {"value": [1, 2, 3]}},
|
||||
"samy": {"samy": {"value": [4, 5, 6]}},
|
||||
"bpm4i": {"bpm4i": {"value": [1, 3, 2]}},
|
||||
},
|
||||
"scan_id": 1,
|
||||
}
|
||||
msg_metadata = {"scan_name": "line_scan"}
|
||||
|
||||
mock_scan_item = mock.MagicMock(spec=ScanItem)
|
||||
mock_scan_item.live_data = {
|
||||
device_name: {
|
||||
entry: mock.MagicMock(val=msg["data"][device_name][entry]["value"])
|
||||
for entry in msg["data"][device_name]
|
||||
}
|
||||
for device_name in msg["data"]
|
||||
}
|
||||
|
||||
w1.queue.scan_storage.find_scan_by_ID.return_value = mock_scan_item
|
||||
|
||||
w1.on_scan_segment(msg, msg_metadata)
|
||||
qtbot.wait(500)
|
||||
|
||||
data = c1.get_data()
|
||||
expected_x_y_data = ([1, 2, 3], [1, 2, 3])
|
||||
expected_z_colors = w1._make_z_gradient([1, 3, 2], "magma")
|
||||
|
||||
scatter_points = c1.scatter.points()
|
||||
colors = [point.brush().color() for point in scatter_points]
|
||||
|
||||
assert np.array_equal(data, expected_x_y_data)
|
||||
assert colors == expected_z_colors
|
||||
|
||||
|
||||
def test_waveform_single_arg_inputs(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
|
||||
w1.plot("bpm4i")
|
||||
w1.plot([1, 2, 3], label="just_y")
|
||||
w1.plot([3, 4, 5], [7, 8, 9], label="x_y")
|
||||
w1.plot(x=[1, 2, 3], y=[4, 5, 6], label="x_y_kwargs")
|
||||
data_array_1D = np.random.rand(10)
|
||||
data_array_2D = np.random.rand(10, 2)
|
||||
w1.plot(data_array_1D, label="np_ndarray 1D")
|
||||
w1.plot(data_array_2D, label="np_ndarray 2D")
|
||||
|
||||
qtbot.wait(200)
|
||||
|
||||
assert w1._curves_data["scan_segment"]["bpm4i-bpm4i"].config.label == "bpm4i-bpm4i"
|
||||
assert w1._curves_data["custom"]["just_y"].config.label == "just_y"
|
||||
assert w1._curves_data["custom"]["x_y"].config.label == "x_y"
|
||||
assert w1._curves_data["custom"]["x_y_kwargs"].config.label == "x_y_kwargs"
|
||||
|
||||
assert np.array_equal(w1._curves_data["custom"]["just_y"].get_data(), ([0, 1, 2], [1, 2, 3]))
|
||||
assert np.array_equal(w1._curves_data["custom"]["just_y"].get_data(), ([0, 1, 2], [1, 2, 3]))
|
||||
assert np.array_equal(w1._curves_data["custom"]["x_y"].get_data(), ([3, 4, 5], [7, 8, 9]))
|
||||
assert np.array_equal(
|
||||
w1._curves_data["custom"]["x_y_kwargs"].get_data(), ([1, 2, 3], [4, 5, 6])
|
||||
)
|
||||
assert np.array_equal(
|
||||
w1._curves_data["custom"]["np_ndarray 1D"].get_data(),
|
||||
(np.arange(data_array_1D.size), data_array_1D.T),
|
||||
)
|
||||
assert np.array_equal(w1._curves_data["custom"]["np_ndarray 2D"].get_data(), data_array_2D.T)
|
||||
|
||||
|
||||
def test_waveform_set_x_sync(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot()
|
||||
custom_label = "custom_label"
|
||||
w1.plot("bpm4i")
|
||||
w1.set_x_label(custom_label)
|
||||
|
||||
scan_item_mock = mock.MagicMock(spec=ScanItem)
|
||||
mock_data = {
|
||||
"samx": {"samx": mock.MagicMock(val=np.array([1, 2, 3]))},
|
||||
"samy": {"samy": mock.MagicMock(val=np.array([4, 5, 6]))},
|
||||
"bpm4i": {
|
||||
"bpm4i": mock.MagicMock(
|
||||
val=np.array([7, 8, 9]),
|
||||
timestamps=np.array([1720520189.959115, 1720520189.986618, 1720520190.0157812]),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
scan_item_mock.live_data = mock_data
|
||||
scan_item_mock.status_message = mock.MagicMock()
|
||||
scan_item_mock.status_message.info = {"scan_report_devices": ["samx"]}
|
||||
|
||||
w1.queue.scan_storage.find_scan_by_ID.return_value = scan_item_mock
|
||||
|
||||
w1.on_scan_segment({"scan_id": 1}, {})
|
||||
qtbot.wait(200)
|
||||
|
||||
# Best effort - samx
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [1, 2, 3])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [auto: samx-samx]"
|
||||
|
||||
# Change to samy
|
||||
w1.set_x("samy")
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [4, 5, 6])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [samy-samy]"
|
||||
|
||||
# change to index
|
||||
w1.set_x("index")
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [index]"
|
||||
|
||||
# change to timestamp
|
||||
w1.set_x("timestamp")
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.allclose(x_data, np.array([1.72052019e09, 1.72052019e09, 1.72052019e09]))
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [timestamp]"
|
||||
|
||||
|
||||
def test_waveform_async_data_update(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot("async_device")
|
||||
custom_label = "custom_label"
|
||||
w1.set_x_label(custom_label)
|
||||
|
||||
# scan_item_mock = mock.MagicMock()
|
||||
# mock_data = {
|
||||
# "async_device": {
|
||||
# "async_device": mock.MagicMock(
|
||||
# val=np.array([7, 8, 9]),
|
||||
# timestamps=np.array([1720520189.959115, 1720520189.986618, 1720520190.0157812]),
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# scan_item_mock.async_data = mock_data
|
||||
# w1.queue.scan_storage.find_scan_by_ID.return_value = scan_item_mock
|
||||
|
||||
msg_1 = {"signals": {"async_device": {"value": [7, 8, 9]}}}
|
||||
metadata_1 = {"async_update": {"max_shape": [None], "type": "add"}}
|
||||
w1.on_async_readback(msg_1, metadata_1)
|
||||
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [best_effort]"
|
||||
|
||||
msg_2 = {"signals": {"async_device": {"value": [10, 11, 12]}}}
|
||||
w1.on_async_readback(msg_2, metadata_1)
|
||||
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2, 3, 4, 5])
|
||||
assert np.array_equal(y_data, [7, 8, 9, 10, 11, 12])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [best_effort]"
|
||||
|
||||
msg_3 = {"signals": {"async_device": {"value": [20, 21, 22]}}}
|
||||
metadata_3 = {"async_update": {"max_shape": [None], "type": "replace"}}
|
||||
w1.on_async_readback(msg_3, metadata_3)
|
||||
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2])
|
||||
assert np.array_equal(y_data, [20, 21, 22])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [best_effort]"
|
||||
|
||||
|
||||
def test_waveform_set_x_async(qtbot, mocked_client):
|
||||
bec_figure = create_widget(qtbot, BECFigure, client=mocked_client)
|
||||
w1 = bec_figure.plot("async_device")
|
||||
custom_label = "custom_label"
|
||||
w1.set_x_label(custom_label)
|
||||
|
||||
scan_item_mock = mock.MagicMock()
|
||||
mock_data = {
|
||||
"async_device": {
|
||||
"async_device": {
|
||||
"value": np.array([7, 8, 9]),
|
||||
"timestamp": np.array([1720520189.959115, 1720520189.986618, 1720520190.0157812]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan_item_mock.async_data = mock_data
|
||||
w1.queue.scan_storage.find_scan_by_ID.return_value = scan_item_mock
|
||||
|
||||
w1.on_scan_status({"scan_id": 1})
|
||||
w1.replot_async_curve()
|
||||
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [best_effort]"
|
||||
|
||||
w1.set_x("timestamp")
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.allclose(x_data, np.array([1.72052019e09, 1.72052019e09, 1.72052019e09]))
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [timestamp]"
|
||||
|
||||
w1.set_x("index")
|
||||
qtbot.wait(200)
|
||||
x_data, y_data = w1.curves[0].get_data()
|
||||
assert np.array_equal(x_data, [0, 1, 2])
|
||||
assert np.array_equal(y_data, [7, 8, 9])
|
||||
assert w1.plot_item.getAxis("bottom").labelText == custom_label + " [index]"
|
Reference in New Issue
Block a user