From f76d9319bd13bb52b1ae2524c1c5e44a167cc330 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 20 Mar 2025 20:46:57 +0100 Subject: [PATCH] refactor(bec_figure): BECFigure removed --- bec_widgets/cli/server.py | 10 +- .../jupyter_console/jupyter_console_window.py | 81 - .../widgets/containers/figure/__init__.py | 1 - .../widgets/containers/figure/figure.py | 789 --------- .../containers/figure/plots/__init__.py | 0 .../containers/figure/plots/axis_settings.py | 91 - .../containers/figure/plots/axis_settings.ui | 256 --- .../containers/figure/plots/image/__init__.py | 0 .../containers/figure/plots/image/image.py | 773 -------- .../figure/plots/image/image_item.py | 338 ---- .../figure/plots/image/image_processor.py | 185 -- .../figure/plots/motor_map/__init__.py | 0 .../figure/plots/motor_map/motor_map.py | 526 ------ .../figure/plots/multi_waveform/__init__.py | 0 .../plots/multi_waveform/multi_waveform.py | 340 ---- .../containers/figure/plots/plot_base.py | 505 ------ .../figure/plots/waveform/__init__.py | 0 .../figure/plots/waveform/waveform.py | 1563 ----------------- .../figure/plots/waveform/waveform_curve.py | 277 --- tests/end-2-end/conftest.py | 22 + tests/end-2-end/test_bec_figure_rpc_e2e.py | 242 --- tests/unit_tests/test_bec_dock.py | 20 - tests/unit_tests/test_bec_figure.py | 275 --- tests/unit_tests/test_bec_image.py | 97 - tests/unit_tests/test_bec_motor_map.py | 282 --- tests/unit_tests/test_multi_waveform.py | 253 --- tests/unit_tests/test_plot_base.py | 247 --- tests/unit_tests/test_plugin_utils.py | 6 +- tests/unit_tests/test_rpc_server.py | 10 +- .../unit_tests/test_utils_plot_indicators.py | 227 +-- tests/unit_tests/test_waveform1d.py | 766 -------- 31 files changed, 146 insertions(+), 8036 deletions(-) delete mode 100644 bec_widgets/widgets/containers/figure/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/figure.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/axis_settings.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/axis_settings.ui delete mode 100644 bec_widgets/widgets/containers/figure/plots/image/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/image/image.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/image/image_item.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/image/image_processor.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/motor_map/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/multi_waveform/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/plot_base.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/waveform/__init__.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/waveform/waveform.py delete mode 100644 bec_widgets/widgets/containers/figure/plots/waveform/waveform_curve.py delete mode 100644 tests/end-2-end/test_bec_figure_rpc_e2e.py delete mode 100644 tests/unit_tests/test_bec_figure.py delete mode 100644 tests/unit_tests/test_bec_image.py delete mode 100644 tests/unit_tests/test_bec_motor_map.py delete mode 100644 tests/unit_tests/test_multi_waveform.py delete mode 100644 tests/unit_tests/test_plot_base.py delete mode 100644 tests/unit_tests/test_waveform1d.py diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py index ccb60deb..131a376b 100644 --- a/bec_widgets/cli/server.py +++ b/bec_widgets/cli/server.py @@ -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." diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index b464e313..ba4ae408 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -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) diff --git a/bec_widgets/widgets/containers/figure/__init__.py b/bec_widgets/widgets/containers/figure/__init__.py deleted file mode 100644 index 7970307e..00000000 --- a/bec_widgets/widgets/containers/figure/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .figure import BECFigure, FigureConfig diff --git a/bec_widgets/widgets/containers/figure/figure.py b/bec_widgets/widgets/containers/figure/figure.py deleted file mode 100644 index ae838ae2..00000000 --- a/bec_widgets/widgets/containers/figure/figure.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/containers/figure/plots/__init__.py b/bec_widgets/widgets/containers/figure/plots/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/containers/figure/plots/axis_settings.py b/bec_widgets/widgets/containers/figure/plots/axis_settings.py deleted file mode 100644 index c54059cd..00000000 --- a/bec_widgets/widgets/containers/figure/plots/axis_settings.py +++ /dev/null @@ -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) diff --git a/bec_widgets/widgets/containers/figure/plots/axis_settings.ui b/bec_widgets/widgets/containers/figure/plots/axis_settings.ui deleted file mode 100644 index dae3a82a..00000000 --- a/bec_widgets/widgets/containers/figure/plots/axis_settings.ui +++ /dev/null @@ -1,256 +0,0 @@ - - - Form - - - - 0 - 0 - 427 - 270 - - - - - 0 - 250 - - - - - 16777215 - 278 - - - - Form - - - - - - - - Plot Title - - - - - - - - - - - - Outer Axes - - - - - - - Y Axis - - - - - - - linear - - - - - log - - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - Min - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - - - - Scale - - - - - - - Label - - - - - - - Max - - - - - - - - - - - - - - Grid - - - - - - - - - - X Axis - - - - - - Scale - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - Min - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - - linear - - - - - log - - - - - - - - Max - - - - - - - - - - Label - - - - - - - - - - - - - - Grid - - - - - - - - - - false - - - - - - - - ToggleSwitch - QWidget -
toggle_switch
-
-
- - -
diff --git a/bec_widgets/widgets/containers/figure/plots/image/__init__.py b/bec_widgets/widgets/containers/figure/plots/image/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/containers/figure/plots/image/image.py b/bec_widgets/widgets/containers/figure/plots/image/image.py deleted file mode 100644 index e879232c..00000000 --- a/bec_widgets/widgets/containers/figure/plots/image/image.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/containers/figure/plots/image/image_item.py b/bec_widgets/widgets/containers/figure/plots/image/image_item.py deleted file mode 100644 index 49f4c631..00000000 --- a/bec_widgets/widgets/containers/figure/plots/image/image_item.py +++ /dev/null @@ -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) diff --git a/bec_widgets/widgets/containers/figure/plots/image/image_processor.py b/bec_widgets/widgets/containers/figure/plots/image/image_processor.py deleted file mode 100644 index b644ef72..00000000 --- a/bec_widgets/widgets/containers/figure/plots/image/image_processor.py +++ /dev/null @@ -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 diff --git a/bec_widgets/widgets/containers/figure/plots/motor_map/__init__.py b/bec_widgets/widgets/containers/figure/plots/motor_map/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py b/bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py deleted file mode 100644 index ea1ff5be..00000000 --- a/bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/containers/figure/plots/multi_waveform/__init__.py b/bec_widgets/widgets/containers/figure/plots/multi_waveform/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py b/bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py deleted file mode 100644 index 038795fd..00000000 --- a/bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/containers/figure/plots/plot_base.py b/bec_widgets/widgets/containers/figure/plots/plot_base.py deleted file mode 100644 index a25b4938..00000000 --- a/bec_widgets/widgets/containers/figure/plots/plot_base.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/containers/figure/plots/waveform/__init__.py b/bec_widgets/widgets/containers/figure/plots/waveform/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/widgets/containers/figure/plots/waveform/waveform.py b/bec_widgets/widgets/containers/figure/plots/waveform/waveform.py deleted file mode 100644 index e86788e6..00000000 --- a/bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +++ /dev/null @@ -1,1563 +0,0 @@ -# pylint: disable=too_many_lines -from __future__ import annotations - -from collections import defaultdict -from typing import Any, Literal, Optional - -import numpy as np -import pyqtgraph as pg -from bec_lib import messages -from bec_lib.device import ReadoutPriority -from bec_lib.endpoints import MessageEndpoints -from bec_lib.logger import bec_logger -from pydantic import Field, ValidationError, field_validator -from pyqtgraph.exporters import MatplotlibExporter -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.utils.colors import get_accent_colors -from bec_widgets.utils.linear_region_selector import LinearRegionWrapper -from bec_widgets.widgets.containers.figure.plots.plot_base import BECPlotBase, SubplotConfig -from bec_widgets.widgets.containers.figure.plots.waveform.waveform_curve import ( - BECCurve, - CurveConfig, - Signal, - SignalData, -) - -logger = bec_logger.logger - - -class Waveform1DConfig(SubplotConfig): - color_palette: Optional[str] = Field( - "magma", description="The color palette of the figure widget.", validate_default=True - ) - curves: dict[str, CurveConfig] = Field( - {}, description="The list of curves to be added to the 1D waveform widget." - ) - - model_config: dict = {"validate_assignment": True} - _validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map) - - -class BECWaveform(BECPlotBase): - READOUT_PRIORITY_HANDLER = { - ReadoutPriority.ON_REQUEST: "on_request", - ReadoutPriority.BASELINE: "baseline", - ReadoutPriority.MONITORED: "monitored", - ReadoutPriority.ASYNC: "async", - ReadoutPriority.CONTINUOUS: "continuous", - } - USER_ACCESS = [ - "_rpc_id", - "_config_dict", - "plot", - "add_dap", - "get_dap_params", - "set_x", - "remove_curve", - "scan_history", - "curves", - "get_curve", - "get_all_data", - "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_scatter", - "enable_fps_monitor", - "lock_aspect_ratio", - "export", - "remove", - "clear_all", - "set_legend_label_size", - "toggle_roi", - "select_roi", - ] - scan_signal_update = pyqtSignal() - async_signal_update = pyqtSignal() - dap_params_update = pyqtSignal(dict, dict) - dap_summary_update = pyqtSignal(dict, dict) - autorange_signal = pyqtSignal() - new_scan = pyqtSignal() - roi_changed = pyqtSignal(tuple) - roi_active = pyqtSignal(bool) - request_dap_refresh = pyqtSignal() - - def __init__( - self, - parent: Optional[QWidget] = None, - parent_figure=None, - config: Optional[Waveform1DConfig] = None, - client=None, - gui_id: Optional[str] = None, - **kwargs, - ): - if config is None: - config = Waveform1DConfig(widget_class=self.__class__.__name__) - super().__init__( - parent=parent, - parent_figure=parent_figure, - config=config, - client=client, - gui_id=gui_id, - **kwargs, - ) - - self._curves_data = defaultdict(dict) - self.old_scan_id = None - self.scan_id = None - self.scan_item = None - self._roi_region = None - self.roi_select = None - self._accent_colors = get_accent_colors() - self._x_axis_mode = { - "name": None, - "entry": None, - "readout_priority": None, - "label_suffix": "", - } - - self._slice_index = None - - # Scan segment update proxy - self.proxy_update_plot = pg.SignalProxy( - self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves - ) - self.proxy_update_dap = pg.SignalProxy( - self.scan_signal_update, rateLimit=25, slot=self.refresh_dap - ) - self.async_signal_update.connect(self.replot_async_curve) - self.autorange_signal.connect(self.auto_range) - - # Get bec shortcuts dev, scans, queue, scan_storage, dap - self.get_bec_shortcuts() - - # Connect dispatcher signals - self.bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment()) - # TODO disabled -> scan_status is SET_AND_PUBLISH -> do not work in combination with autoupdate from CLI - # self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status()) - - self.entry_validator = EntryValidator(self.dev) - - self.add_legend() - self.apply_config(self.config) - - @Slot(bool) - def toggle_roi(self, toggled: bool) -> None: - """Toggle the linear region selector on the plot. - - Args: - toggled(bool): If True, enable the linear region selector. - """ - if toggled: - return self._hook_roi() - return self._unhook_roi() - - @Slot(tuple) - def select_roi(self, region: tuple[float, float]): - """Set the fit region of the plot widget. At the moment only a single region is supported. - To remove the roi region again, use toggle_roi_region - - Args: - region(tuple[float, float]): The fit region. - """ - if self.roi_region == (None, None): - self.toggle_roi(True) - try: - self.roi_select.linear_region_selector.setRegion(region) - except Exception as e: - logger.error(f"Error setting region {tuple}; Exception raised: {e}") - raise ValueError(f"Error setting region {tuple}; Exception raised: {e}") from e - - def _hook_roi(self): - """Hook the linear region selector to the plot.""" - color = self._accent_colors.default - color.setAlpha(int(0.2 * 255)) - hover_color = self._accent_colors.default - hover_color.setAlpha(int(0.35 * 255)) - if self.roi_select is None: - self.roi_select = LinearRegionWrapper( - self.plot_item, color=color, hover_color=hover_color, parent=self - ) - self.roi_select.add_region_selector() - self.roi_select.region_changed.connect(self.roi_changed) - self.roi_select.region_changed.connect(self.set_roi_region) - self.request_dap_refresh.connect(self.refresh_dap) - self._emit_roi_region() - self.roi_active.emit(True) - - def _unhook_roi(self): - """Unhook the linear region selector from the plot.""" - if self.roi_select is not None: - self.roi_select.region_changed.disconnect(self.roi_changed) - self.roi_select.region_changed.disconnect(self.set_roi_region) - self.request_dap_refresh.disconnect(self.refresh_dap) - self.roi_active.emit(False) - self.roi_region = None - self.refresh_dap() - self.roi_select.cleanup() - self.roi_select.deleteLater() - self.roi_select = None - - def apply_config(self, config: dict | SubplotConfig, replot_last_scan: bool = False): - """ - 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 = Waveform1DConfig(**config) - except ValidationError as e: - logger.error(f"Validation error when applying config to BECWaveform1D: {e}") - return - - self.config = config - self.plot_item.clear() # TODO not sure if on the plot or layout level - - self.apply_axis_config() - # Reset curves - self._curves_data = defaultdict(dict) - self._curves = self.plot_item.curves - for curve_config in self.config.curves.values(): - self.add_curve_by_config(curve_config) - if replot_last_scan: - self.scan_history(scan_index=-1) - - def change_gui_id(self, new_gui_id: str): - """ - Change the GUI ID of the waveform widget and update the parent_id in all associated curves. - - Args: - new_gui_id (str): The new GUI ID to be set for the waveform widget. - """ - # Update the gui_id in the waveform widget itself - self.gui_id = new_gui_id - self.config.gui_id = new_gui_id - - for curve in self.curves: - curve.config.parent_id = new_gui_id - - ################################### - # Fit Range Properties - ################################### - - @property - def roi_region(self) -> tuple[float, float] | None: - """ - Get the fit region of the plot widget. - - Returns: - tuple: The fit region. - """ - if self._roi_region is not None: - return self._roi_region - return None, None - - @roi_region.setter - def roi_region(self, value: tuple[float, float] | None): - """Set the fit region of the plot widget. - - Args: - value(tuple[float, float]|None): The fit region. - """ - self._roi_region = value - if value is not None: - self.request_dap_refresh.emit() - - @Slot(tuple) - def set_roi_region(self, region: tuple[float, float]): - """ - Set the fit region of the plot widget. - - Args: - region(tuple[float, float]): The fit region. - """ - self.roi_region = region - - def _emit_roi_region(self): - """Emit the current ROI from selector the plot widget.""" - if self.roi_select is not None: - self.set_roi_region(self.roi_select.linear_region_selector.getRegion()) - - ################################### - # Waveform Properties - ################################### - - @property - def curves(self) -> list[BECCurve]: - """ - Get the curves of the plot widget as a list - Returns: - list: List of curves. - """ - return self._curves - - @curves.setter - def curves(self, value: list[BECCurve]): - self._curves = value - - @property - def x_axis_mode(self) -> dict: - """ - Get the x axis mode of the plot widget. - - Returns: - dict: The x axis mode. - """ - return self._x_axis_mode - - @x_axis_mode.setter - def x_axis_mode(self, value: dict): - self._x_axis_mode = value - - ################################### - # Adding and Removing Curves - ################################### - - def add_curve_by_config(self, curve_config: CurveConfig | dict) -> BECCurve: - """ - Add a curve to the plot widget by its configuration. - - Args: - curve_config(CurveConfig|dict): Configuration of the curve to be added. - - Returns: - BECCurve: The curve object. - """ - if isinstance(curve_config, dict): - curve_config = CurveConfig(**curve_config) - curve = self._add_curve_object( - name=curve_config.label, source=curve_config.source, config=curve_config - ) - return curve - - def get_curve_config(self, curve_id: str, dict_output: bool = True) -> CurveConfig | dict: - """ - Get the configuration of a curve by its ID. - - Args: - curve_id(str): ID of the curve. - - Returns: - CurveConfig|dict: Configuration of the curve. - """ - for curves in self._curves_data.values(): - if curve_id in curves: - if dict_output: - return curves[curve_id].config.model_dump() - else: - return curves[curve_id].config - - def get_curve(self, identifier) -> BECCurve: - """ - Get the curve by its index or ID. - - Args: - identifier(int|str): Identifier of the curve. Can be either an integer (index) or a string (curve_id). - - Returns: - BECCurve: The curve object. - """ - if isinstance(identifier, int): - return self.plot_item.curves[identifier] - elif isinstance(identifier, str): - for curves in self._curves_data.values(): - if identifier in curves: - return curves[identifier] - raise ValueError(f"Curve with ID '{identifier}' not found.") - else: - raise ValueError("Identifier must be either an integer (index) or a string (curve_id).") - - def enable_scatter(self, enable: bool): - """ - Enable/Disable scatter plot on all curves. - - Args: - enable(bool): If True, enable scatter markers; if False, disable them. - """ - for curve in self.curves: - if isinstance(curve, BECCurve): - if enable: - curve.set_symbol("o") # You can choose any symbol you like - else: - curve.set_symbol(None) - - 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, - dap: str | None = None, # TODO add dap custom curve wrapper - **kwargs, - ) -> BECCurve: - """ - Plot a curve to the plot widget. - - Args: - arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name. - y(list | np.ndarray): Custom y data to plot. - x(list | np.ndarray): Custom y data to plot. - x_name(str): Name of the x signal. - - "best_effort": Use the best effort signal. - - "timestamp": Use the timestamp signal. - - "index": Use the index signal. - - Custom signal name of device from BEC. - 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. - dap(str): The dap model to use for the curve, only available for sync devices. If not specified, none will be added. - - Returns: - BECCurve: The curve object. - """ - if x is not None and y is not None: - return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs) - - if isinstance(arg1, str): - y_name = arg1 - elif isinstance(arg1, list): - if isinstance(y, list): - return self.add_curve_custom(x=arg1, y=y, label=label, color=color, **kwargs) - if y is None: - x = np.arange(len(arg1)) - return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs) - elif isinstance(arg1, np.ndarray) and y is None: - if arg1.ndim == 1: - x = np.arange(arg1.size) - return self.add_curve_custom(x=x, y=arg1, label=label, color=color, **kwargs) - if arg1.ndim == 2: - x = arg1[:, 0] - y = arg1[:, 1] - return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs) - if y_name is None: - raise ValueError("y_name must be provided.") - if dap: - self.add_dap(x_name=x_name, y_name=y_name, dap=dap) - curve = self.add_curve_bec( - 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_bec=validate, - **kwargs, - ) - self.scan_signal_update.emit() - self.async_signal_update.emit() - - return curve - - def set_x(self, x_name: str, x_entry: str | None = None): - """ - Change the x axis of the plot widget. - - Args: - x_name(str): Name of the x signal. - - "best_effort": Use the best effort signal. - - "timestamp": Use the timestamp signal. - - "index": Use the index signal. - - Custom signal name of device from BEC. - x_entry(str): Entry of the x signal. - """ - if not x_name: - # this can happen, if executed by a signal from a widget - return - - curve_configs = self.config.curves - curve_ids = list(curve_configs.keys()) - curve_configs = list(curve_configs.values()) - self.set_auto_range(True, "xy") - - x_entry, _, _ = self._validate_signal_entries( - x_name, None, None, x_entry, None, None, validate_bec=True - ) - - readout_priority_x = None - if x_name not in ["best_effort", "timestamp", "index"]: - readout_priority_x = self._get_device_readout_priority(x_name) - - self.x_axis_mode = { - "name": x_name, - "entry": x_entry, - "readout_priority": readout_priority_x, - } - - if len(self.curves) > 0: - # validate all curves - for curve in self.curves: - if not isinstance(curve, BECCurve): - continue - if curve.config.source == "custom": - continue - self._validate_x_axis_behaviour(curve.config.signals.y.name, x_name, x_entry, False) - self._switch_x_axis_item( - f"{x_name}-{x_entry}" - if x_name not in ["best_effort", "timestamp", "index"] - else x_name - ) - for curve_id, curve_config in zip(curve_ids, curve_configs): - if curve_config.signals is None: - continue - if curve_config.signals.x is None: - continue - curve_config.signals.x.name = x_name - curve_config.signals.x.entry = x_entry - self.remove_curve(curve_id) - self.add_curve_by_config(curve_config) - - self.async_signal_update.emit() - self.scan_signal_update.emit() - - @Slot() - def auto_range(self): - """Manually set auto range of the plotitem""" - self.plot_item.autoRange() - - 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) - - def add_curve_custom( - self, - x: list | np.ndarray, - y: list | np.ndarray, - label: str = None, - color: str = None, - curve_source: str = "custom", - **kwargs, - ) -> BECCurve: - """ - Add a custom data curve to the plot widget. - - Args: - x(list|np.ndarray): X data of the curve. - y(list|np.ndarray): Y data of the curve. - label(str, optional): Label of the curve. Defaults to None. - color(str, optional): Color of the curve. Defaults to None. - curve_source(str, optional): Tag for source of the curve. Defaults to "custom". - **kwargs: Additional keyword arguments for the curve configuration. - - Returns: - BECCurve: The curve object. - """ - curve_id = label or f"Curve {len(self.plot_item.curves) + 1}" - - curve_exits = self._check_curve_id(curve_id, self._curves_data) - if curve_exits: - raise ValueError( - f"Curve with ID '{curve_id}' already exists in widget '{self.gui_id}'." - ) - - color = ( - color - or Colors.golden_angle_color( - colormap=self.config.color_palette, - num=max(10, len(self.plot_item.curves) + 1), - format="HEX", - )[len(self.plot_item.curves)] - ) - - # Create curve by config - curve_config = CurveConfig( - widget_class="BECCurve", - parent_id=self.gui_id, - label=curve_id, - color=color, - source=curve_source, - **kwargs, - ) - - curve = self._add_curve_object( - name=curve_id, source=curve_source, config=curve_config, data=(x, y) - ) - return curve - - def add_curve_bec( - self, - 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_bec: bool = True, - dap: str | None = None, - source: str | None = None, - **kwargs, - ) -> BECCurve: - """ - Add a curve to the plot widget from the scan segment. #TODO adapt docs to DAP - - Args: - x_name(str): Name of the x signal. - x_entry(str): Entry of the x signal. - y_name(str): Name of the y signal. - y_entry(str): Entry of the y signal. - z_name(str): Name of the z signal. - z_entry(str): Entry of the z signal. - color(str, optional): Color of the curve. Defaults to None. - color_map_z(str): The color map to use for the z-axis. - label(str, optional): Label of the curve. Defaults to None. - validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True. - dap(str, optional): The dap model to use for the curve. Defaults to None. - **kwargs: Additional keyword arguments for the curve configuration. - - Returns: - BECCurve: The curve object. - """ - # 1. Check - y_name must be provided - if y_name is None: - raise ValueError("y_name must be provided.") - - # 2. Check - check if there is already a x axis signal - if x_name is None: - mode = self.x_axis_mode["name"] - x_name = mode if mode is not None else "best_effort" - self.x_axis_mode["name"] = x_name - - if not x_name or not y_name: - # can happen if executed from a signal from a widget ; - # the code above has to be executed to set some other - # variables, but it cannot continue if both names are - # not set properly -> exit here - return - - # 3. Check - Get entry if not provided and validate - x_entry, y_entry, z_entry = self._validate_signal_entries( - x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec - ) - - # 4. Check - get source of the device - if source is None: - if validate_bec is True: - source = self._validate_device_source_compatibity(y_name) - else: - source = "scan_segment" - - if z_name is not None and z_entry is not None: - label = label or f"{z_name}-{z_entry}" - else: - label = label or f"{y_name}-{y_entry}" - - # 5. Check - Check if curve already exists - curve_exits = self._check_curve_id(label, self._curves_data) - if curve_exits: - raise ValueError(f"Curve with ID '{label}' already exists in widget '{self.gui_id}'.") - - # Validate or define x axis behaviour and compatibility with y_name readoutPriority - if validate_bec is True: - self._validate_x_axis_behaviour(y_name, x_name, x_entry) - - # Create color if not specified - color = ( - color - or Colors.golden_angle_color( - colormap=self.config.color_palette, - num=max(10, len(self.plot_item.curves) + 1), - format="HEX", - )[len(self.plot_item.curves)] - ) - logger.info(f"Color: {color}") - - # Create curve by config - curve_config = CurveConfig( - widget_class="BECCurve", - parent_id=self.gui_id, - label=label, - color=color, - color_map_z=color_map_z, - source=source, - signals=Signal( - source=source, - x=SignalData(name=x_name, entry=x_entry) if x_name else None, - y=SignalData(name=y_name, entry=y_entry), - z=SignalData(name=z_name, entry=z_entry) if z_name else None, - dap=dap, - ), - **kwargs, - ) - - curve = self._add_curve_object(name=label, source=source, config=curve_config) - return curve - - def add_dap( - self, - x_name: str | None = None, - y_name: str | None = None, - x_entry: Optional[str] = None, - y_entry: Optional[str] = None, - color: Optional[str] = None, - dap: str = "GaussianModel", - validate_bec: bool = True, - **kwargs, - ) -> BECCurve: - """ - Add LMFIT dap model curve to the plot widget. - - Args: - x_name(str): Name of the x signal. - x_entry(str): Entry of the x signal. - y_name(str): Name of the y signal. - y_entry(str): Entry of the y signal. - color(str, optional): Color of the curve. Defaults to None. - dap(str): The dap model to use for the curve. - validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True. - **kwargs: Additional keyword arguments for the curve configuration. - - Returns: - BECCurve: The curve object. - """ - if x_name is None: - x_name = self.x_axis_mode["name"] - x_entry = self.x_axis_mode["entry"] - if x_name == "timestamp" or x_name == "index": - raise ValueError( - f"Cannot use x axis '{x_name}' for DAP curve. Please provide a custom x axis signal or switch to 'best_effort' signal mode." - ) - - if self.x_axis_mode["readout_priority"] == "async": - raise ValueError( - "Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals." - ) - - if validate_bec is True: - x_entry, y_entry, _ = self._validate_signal_entries( - x_name, y_name, None, x_entry, y_entry, None - ) - label = f"{y_name}-{y_entry}-{dap}" - curve = self.add_curve_bec( - x_name=x_name, - y_name=y_name, - x_entry=x_entry, - y_entry=y_entry, - color=color, - label=label, - source="DAP", - dap=dap, - symbol="star", - **kwargs, - ) - - self.setup_dap(self.old_scan_id, self.scan_id) - self.refresh_dap() - return curve - - @Slot() - def get_dap_params(self) -> dict: - """ - Get the DAP parameters of all DAP curves. - - Returns: - dict: DAP parameters of all DAP curves. - """ - params = {} - for curve_id, curve in self._curves_data["DAP"].items(): - params[curve_id] = curve.dap_params - return params - - @Slot() - def get_dap_summary(self) -> dict: - """ - Get the DAP summary of all DAP curves. - - Returns: - dict: DAP summary of all DAP curves. - """ - summary = {} - for curve_id, curve in self._curves_data["DAP"].items(): - summary[curve_id] = curve.dap_summary - return summary - - def _add_curve_object( - self, - name: str, - source: str, - config: CurveConfig, - data: tuple[list | np.ndarray, list | np.ndarray] = None, - ) -> BECCurve: - """ - Add a curve object to the plot widget. - - Args: - name(str): ID of the curve. - source(str): Source of the curve. - config(CurveConfig): Configuration of the curve. - data(tuple[list|np.ndarray,list|np.ndarray], optional): Data (x,y) to be plotted. Defaults to None. - - Returns: - BECCurve: The curve object. - """ - curve = BECCurve(config=config, name=name, parent_item=self) - self._curves_data[source][name] = curve - self.plot_item.addItem(curve) - self.config.curves[name] = curve.config - if data is not None: - curve.setData(data[0], data[1]) - self.set_legend_label_size() - return curve - - def _validate_device_source_compatibity(self, name: str): - readout_priority_y = self._get_device_readout_priority(name) - if readout_priority_y == "monitored" or readout_priority_y == "baseline": - source = "scan_segment" - elif readout_priority_y == "async": - source = "async" - else: - raise ValueError( - f"Readout priority '{readout_priority_y}' of device '{name}' is not supported for y signal." - ) - return source - - def _validate_x_axis_behaviour( - self, y_name: str, x_name: str | None = None, x_entry: str | None = None, auto_switch=True - ) -> None: - """ - Validate the x axis behaviour and consistency for the plot item. - - Args: - source(str): Source of updating device. Can be either "scan_segment" or "async". - x_name(str): Name of the x signal. - - "best_effort": Use the best effort signal. - - "timestamp": Use the timestamp signal. - - "index": Use the index signal. - - Custom signal name of device from BEC. - x_entry(str): Entry of the x signal. - """ - - readout_priority_y = self._get_device_readout_priority(y_name) - - # Check if the x axis behaviour is already set - if self._x_axis_mode["name"] is not None: - # Case 1: The same x axis signal is used, check if source is compatible with the device - if x_name != self._x_axis_mode["name"] and x_entry != self._x_axis_mode["entry"]: - # A different x axis signal is used, raise an exception - raise ValueError( - f"All curves must have the same x axis.\n" - f" Current valid x axis: '{self._x_axis_mode['name']}'\n" - f" Attempted to add curve with x axis: '{x_name}'\n" - f"If you want to change the x-axis of the curve, please remove previous curves or change the x axis of the plot widget with '.set_x({x_name})'." - ) - - # If x_axis_mode["name"] is None, determine the mode based on x_name - # With async the best effort is always "index" - # Setting mode to either "best_effort", "timestamp", "index", or a custom one - if x_name is None and readout_priority_y == "async": - x_name = "index" - x_entry = "index" - if x_name in ["best_effort", "timestamp", "index"]: - self._x_axis_mode["name"] = x_name - self._x_axis_mode["entry"] = x_entry - else: - self._x_axis_mode["name"] = x_name - self._x_axis_mode["entry"] = x_entry - if readout_priority_y == "async": - raise ValueError( - f"Async devices '{y_name}' cannot be used with custom x signal '{x_name}-{x_entry}'.\n" - f"Please use mode 'best_effort', 'timestamp', or 'index' signal for x axis." - f"You can change the x axis mode with '.set_x(mode)'" - ) - - if auto_switch is True: - # Switch the x axis mode accordingly - self._switch_x_axis_item( - f"{x_name}-{x_entry}" - if x_name not in ["best_effort", "timestamp", "index"] - else x_name - ) - - def _get_device_readout_priority(self, name: str): - """ - Get the type of device from the entry_validator. - - Args: - name(str): Name of the device. - entry(str): Entry of the device. - - Returns: - str: Type of the device. - """ - return self.READOUT_PRIORITY_HANDLER[self.dev[name].readout_priority] - - def _switch_x_axis_item(self, mode: str): - """ - Switch the x-axis mode between timestamp, index, the best effort and custom signal. - - Args: - mode(str): Mode of the x-axis. - - "timestamp": Use the timestamp signal. - - "index": Use the index signal. - - "best_effort": Use the best effort signal. - - Custom signal name of device from BEC. - """ - current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label - date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation="bottom") - default_axis = pg.AxisItem(orientation="bottom") - self._x_axis_mode["label_suffix"] = f" [{mode}]" - - if mode == "timestamp": - self.plot_item.setAxisItems({"bottom": date_axis}) - self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}") - elif mode == "index": - self.plot_item.setAxisItems({"bottom": default_axis}) - self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}") - else: - self.plot_item.setAxisItems({"bottom": default_axis}) - self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}") - - def _validate_signal_entries( - self, - x_name: str | None, - y_name: str | None, - z_name: str | None, - x_entry: str | None, - y_entry: str | None, - z_entry: str | None, - validate_bec: bool = True, - ) -> tuple[str, str, str | None]: - """ - Validate the signal name and entry. - - Args: - x_name(str): Name of the x signal. - y_name(str): Name of the y signal. - z_name(str): Name of the z signal. - x_entry(str|None): Entry of the x signal. - y_entry(str|None): Entry of the y signal. - z_entry(str|None): Entry of the z signal. - validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True. - - Returns: - tuple[str,str,str|None]: Validated x, y, z entries. - """ - if validate_bec: - if x_name is None: - x_name = "best_effort" - x_entry = "best_effort" - if x_name: - if x_name == "index" or x_name == "timestamp" or x_name == "best_effort": - x_entry = x_name - else: - x_entry = self.entry_validator.validate_signal(x_name, x_entry) - if y_name: - y_entry = self.entry_validator.validate_signal(y_name, y_entry) - if z_name: - z_entry = self.entry_validator.validate_signal(z_name, z_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 - z_entry = z_name if z_entry is None else z_entry - return x_entry, y_entry, z_entry - - def _check_curve_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_curve_id(val, dict_to_check[key]): - return True - return False - - def remove_curve(self, *identifiers): - """ - Remove a curve from the plot widget. - - Args: - *identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id). - """ - for identifier in identifiers: - if isinstance(identifier, int): - self._remove_curve_by_order(identifier) - elif isinstance(identifier, str): - self._remove_curve_by_id(identifier) - else: - raise ValueError( - "Each identifier must be either an integer (index) or a string (curve_id)." - ) - - def _remove_curve_by_id(self, curve_id): - """ - Remove a curve by its ID from the plot widget. - - Args: - curve_id(str): ID of the curve to be removed. - """ - for curves in self._curves_data.values(): - if curve_id in curves: - curve = curves.pop(curve_id) - self.plot_item.removeItem(curve) - del self.config.curves[curve_id] - if curve in self.plot_item.curves: - self.plot_item.curves.remove(curve) - return - raise KeyError(f"Curve with ID '{curve_id}' not found.") - - def _remove_curve_by_order(self, N): - """ - Remove a curve by its order from the plot widget. - - Args: - N(int): Order of the curve to be removed. - """ - if N < len(self.plot_item.curves): - curve = self.plot_item.curves[N] - curve_id = curve.name() # Assuming curve's name is used as its ID - self.plot_item.removeItem(curve) - del self.config.curves[curve_id] - # Remove from self.curve_data - for curves in self._curves_data.values(): - if curve_id in curves: - del curves[curve_id] - break - else: - raise IndexError(f"Curve order {N} out of range.") - - @Slot(dict) - def on_scan_status(self, msg): - """ - Handle the scan status message. - - Args: - msg(dict): Message received with scan status. - """ - - current_scan_id = msg.get("scan_id", None) - if current_scan_id is None: - return - - if current_scan_id != self.scan_id: - self.reset() - self.new_scan.emit() - self.set_auto_range(True, "xy") - self.old_scan_id = self.scan_id - self.scan_id = current_scan_id - self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id) - if self._curves_data["DAP"]: - self.setup_dap(self.old_scan_id, self.scan_id) - if self._curves_data["async"]: - for curve in self._curves_data["async"].values(): - self.setup_async( - name=curve.config.signals.y.name, entry=curve.config.signals.y.entry - ) - - @Slot(dict, dict) - def on_scan_segment(self, msg: dict, metadata: dict): - """ - Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher. - Used only for triggering scan segment update from the BECClient scan storage. - - Args: - msg (dict): Message received with scan data. - metadata (dict): Metadata of the scan. - """ - self.on_scan_status(msg) - self.scan_signal_update.emit() - # self.autorange_timer.start(100) - - 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. - """ - super().set_x_label(label, size) - current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label - self.plot_item.setLabel("bottom", f"{current_label}{self._x_axis_mode['label_suffix']}") - - def set_colormap(self, colormap: str | None = None): - """ - Set the colormap of the plot widget. - - Args: - colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette. - """ - if colormap is not None: - self.config.color_palette = colormap - - colors = Colors.golden_angle_color( - colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX" - ) - for curve, color in zip(self.curves, colors): - curve.set_color(color) - - def setup_dap(self, old_scan_id: str | None, new_scan_id: str | None): - """ - Setup DAP for the new scan. - - Args: - old_scan_id(str): old_scan_id, used to disconnect the previous dispatcher connection. - new_scan_id(str): new_scan_id, used to connect the new dispatcher connection. - - """ - self.bec_dispatcher.disconnect_slot( - self.update_dap, MessageEndpoints.dap_response(f"{old_scan_id}-{self.gui_id}") - ) - if len(self._curves_data["DAP"]) > 0: - self.bec_dispatcher.connect_slot( - self.update_dap, MessageEndpoints.dap_response(f"{new_scan_id}-{self.gui_id}") - ) - - @Slot(str) - def setup_async(self, name: str, entry: str): - self.bec_dispatcher.disconnect_slot( - self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, name) - ) - try: - self._curves_data["async"][f"{name}-{entry}"].clear_data() - except KeyError: - pass - if len(self._curves_data["async"]) > 0: - self.bec_dispatcher.connect_slot( - self.on_async_readback, - MessageEndpoints.device_async_readback(self.scan_id, name), - from_start=True, - ) - - @Slot() - def refresh_dap(self, _=None): - """ - Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response(). - """ - for curve_id, curve in self._curves_data["DAP"].items(): - if len(self._curves_data["async"]) > 0: - curve.remove() - raise ValueError( - f"Cannot refresh DAP curve '{curve_id}' while async curves are present. Removing {curve_id} from display." - ) - if self._x_axis_mode["name"] == "best_effort": - try: - x_name = self.scan_item.status_message.info["scan_report_devices"][0] - x_entry = self.entry_validator.validate_signal(x_name, None) - except AttributeError: - return - elif curve.config.signals.x is not None: - x_name = curve.config.signals.x.name - x_entry = curve.config.signals.x.entry - if ( - x_name == "timestamp" or x_name == "index" - ): # timestamp and index not supported by DAP - return - try: # to prevent DAP update if the x axis is not the same as the current scan - current_x_names = self.scan_item.status_message.info["scan_report_devices"] - if x_name not in current_x_names: - return - except AttributeError: - return - - y_name = curve.config.signals.y.name - y_entry = curve.config.signals.y.entry - model_name = curve.config.signals.dap - model = getattr(self.dap, model_name) - x_min, x_max = self.roi_region - - msg = messages.DAPRequestMessage( - dap_cls="LmfitService1D", - dap_type="on_demand", - config={ - "args": [self.scan_id, x_name, x_entry, y_name, y_entry], - "kwargs": {"x_min": x_min, "x_max": x_max}, - "class_args": model._plugin_info["class_args"], - "class_kwargs": model._plugin_info["class_kwargs"], - }, - metadata={"RID": f"{self.scan_id}-{self.gui_id}"}, - ) - self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg) - - @Slot(dict, dict) - def update_dap(self, msg, metadata): - """Callback for DAP response message.""" - - # pylint: disable=unused-variable - scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"] - model = msg["dap_request"].content["config"]["class_kwargs"]["model"] - - curve_id_request = f"{y_name}-{y_entry}-{model}" - - for curve_id, curve in self._curves_data["DAP"].items(): - if curve_id == curve_id_request: - if msg["data"] is not None: - x = msg["data"][0]["x"] - y = msg["data"][0]["y"] - curve.setData(x, y) - curve.dap_params = msg["data"][1]["fit_parameters"] - curve.dap_summary = msg["data"][1]["fit_summary"] - metadata.update({"curve_id": curve_id_request}) - self.dap_params_update.emit(curve.dap_params, metadata) - self.dap_summary_update.emit(curve.dap_summary, metadata) - break - - @Slot(dict, dict) - def on_async_readback(self, msg, metadata): - """ - Get async data readback. - - Args: - msg(dict): Message with the async data. - metadata(dict): Metadata of the message. - """ - y_data = None - x_data = None - instruction = metadata.get("async_update", {}).get("type") - max_shape = metadata.get("async_update", {}).get("max_shape", []) - all_async_curves = self._curves_data["async"].values() - # for curve in self._curves_data["async"].values(): - for curve in all_async_curves: - y_entry = curve.config.signals.y.entry - x_name = self._x_axis_mode["name"] - for device, async_data in msg["signals"].items(): - if device == y_entry: - data_plot = async_data["value"] - if instruction == "add": - if len(max_shape) > 1: - if len(data_plot.shape) > 1: - data_plot = data_plot[-1, :] - else: - x_data, y_data = curve.get_data() - if y_data is not None: - new_data = np.hstack((y_data, data_plot)) - else: - new_data = data_plot - if x_name == "timestamp": - if x_data is not None: - x_data = np.hstack((x_data, async_data["timestamp"])) - else: - x_data = async_data["timestamp"] - curve.setData(x_data, new_data) - else: - curve.setData(new_data) - elif instruction == "add_slice": - current_slice_id = metadata.get("async_update", {}).get("index") - data_plot = async_data["value"] - if current_slice_id != self._slice_index: - self._slice_index = current_slice_id - new_data = data_plot - else: - x_data, y_data = curve.get_data() - new_data = np.hstack((y_data, data_plot)) - - curve.setData(new_data) - - elif instruction == "replace": - if x_name == "timestamp": - x_data = async_data["timestamp"] - curve.setData(x_data, data_plot) - else: - curve.setData(data_plot) - - @Slot() - def replot_async_curve(self): - try: - data = self.scan_item.async_data - except AttributeError: - return - for curve_id, curve in self._curves_data["async"].items(): - y_name = curve.config.signals.y.name - y_entry = curve.config.signals.y.entry - x_name = None - - if curve.config.signals.x: - x_name = curve.config.signals.x.name - - if x_name == "timestamp": - data_x = data[y_name][y_entry]["timestamp"] - else: - data_x = None - data_y = data[y_name][y_entry]["value"] - - if data_x is None: - curve.setData(data_y) - else: - curve.setData(data_x, data_y) - - @Slot() - def _update_scan_curves(self, _=None): - """ - Update the scan curves with the data from the scan segment. - """ - try: - data = ( - self.scan_item.live_data - if hasattr(self.scan_item, "live_data") # backward compatibility - else self.scan_item.data - ) - except AttributeError: - return - - data_x = None - data_y = None - data_z = None - - for curve_id, curve in self._curves_data["scan_segment"].items(): - - y_name = curve.config.signals.y.name - y_entry = curve.config.signals.y.entry - if curve.config.signals.z: - z_name = curve.config.signals.z.name - z_entry = curve.config.signals.z.entry - - data_x = self._get_x_data(curve, y_name, y_entry) - if len(data) == 0: # case if the data is empty because motor is not scanned - return - - try: - data_y = data[y_name][y_entry].val - if curve.config.signals.z: - data_z = data[z_name][z_entry].val - color_z = self._make_z_gradient(data_z, curve.config.color_map_z) - except TypeError: - continue - - if data_z is not None and color_z is not None: - try: - curve.setData(x=data_x, y=data_y, symbolBrush=color_z) - except: - return - if data_x is None: - curve.setData(data_y) - else: - curve.setData(data_x, data_y) - - def _get_x_data(self, curve: BECCurve, y_name: str, y_entry: str) -> list | np.ndarray | None: - """ - Get the x data for the curve with the decision logic based on the curve configuration: - - If x is called 'timestamp', use the timestamp data from the scan item. - - If x is called 'index', use the rolling index. - - If x is a custom signal, use the data from the scan item. - - If x is not specified, use the first device from the scan report. - - Args: - curve(BECCurve): The curve object. - - Returns: - list|np.ndarray|None: X data for the curve. - """ - x_data = None - live_data = ( - self.scan_item.live_data - if hasattr(self.scan_item, "live_data") - else self.scan_item.data - ) - if self._x_axis_mode["name"] == "timestamp": - - timestamps = live_data[y_name][y_entry].timestamps - - x_data = timestamps - return x_data - if self._x_axis_mode["name"] == "index": - x_data = None - return x_data - - if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort": - if len(self._curves_data["async"]) > 0: - x_data = None - self._x_axis_mode["label_suffix"] = " [auto: index]" - current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label - self.plot_item.setLabel( - "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}" - ) - return x_data - else: - x_name = self.scan_item.status_message.info["scan_report_devices"][0] - x_entry = self.entry_validator.validate_signal(x_name, None) - x_data = live_data[x_name][x_entry].val - self._x_axis_mode["label_suffix"] = f" [auto: {x_name}-{x_entry}]" - current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label - self.plot_item.setLabel( - "bottom", f"{current_label}{self._x_axis_mode['label_suffix']}" - ) - - else: - x_name = curve.config.signals.x.name - x_entry = curve.config.signals.x.entry - try: - x_data = live_data[x_name][x_entry].val - except TypeError: - x_data = [] - return x_data - - def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None: - """ - Make a gradient color for the z values. - - Args: - data_z(list|np.ndarray): Z values. - colormap(str): Colormap for the gradient color. - - Returns: - list: List of colors for the z values. - """ - # Normalize z_values for color mapping - z_min, z_max = np.min(data_z), np.max(data_z) - - if z_max != z_min: # Ensure that there is a range in the z values - z_values_norm = (data_z - z_min) / (z_max - z_min) - colormap = pg.colormap.get(colormap) # using colormap from global settings - colors = [colormap.map(z, mode="qcolor") for z in z_values_norm] - return colors - else: - return None - - def scan_history(self, scan_index: int = None, scan_id: str = None): - """ - Update the scan curves with the data from the scan storage. - Provide only one of scan_id or scan_index. - - Args: - scan_id(str, optional): ScanID of the scan to be updated. Defaults to None. - scan_index(int, optional): Index of the scan to be updated. Defaults to None. - """ - if scan_index is not None and scan_id is not None: - raise ValueError("Only one of scan_id or scan_index can be provided.") - - # Reset DAP connector - self.bec_dispatcher.disconnect_slot( - self.update_dap, MessageEndpoints.dap_response(self.scan_id) - ) - if scan_index is not None: - try: - self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id - except IndexError: - logger.error(f"Scan index {scan_index} out of range.") - return - elif scan_id is not None: - self.scan_id = scan_id - - self.setup_dap(self.old_scan_id, self.scan_id) - self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id) - self.scan_signal_update.emit() - self.async_signal_update.emit() - - def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame: - """ - 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" - - for curve in self.plot_item.curves: - x_data, y_data = curve.get_data() - if x_data is not None or y_data is not None: - if output == "dict": - data[curve.name()] = {"x": x_data.tolist(), "y": y_data.tolist()} - elif output == "pandas" and pd is not None: - data[curve.name()] = pd.DataFrame({"x": x_data, "y": y_data}) - - if output == "pandas" and pd is not None: - combined_data = pd.concat( - [data[curve.name()] for curve in self.plot_item.curves], - axis=1, - keys=[curve.name() for curve in self.plot_item.curves], - ) - return combined_data - return data - - def export_to_matplotlib(self): - """ - Export current waveform to matplotlib gui. Available only if matplotlib is installed in the enviroment. - - """ - MatplotlibExporter(self.plot_item).export() - - def clear_source(self, source: Literal["DAP", "async", "scan_segment", "custom"]): - """Clear speicific source from self._curves_data. - - Args: - source (Literal["DAP", "async", "scan_segment", "custom"]): Source to be cleared. - """ - curves_data = self._curves_data - curve_ids_to_remove = list(curves_data[source].keys()) - for curve_id in curve_ids_to_remove: - self.remove_curve(curve_id) - - def reset(self): - self._slice_index = None - super().reset() - - def clear_all(self): - sources = list(self._curves_data.keys()) - for source in sources: - self.clear_source(source) - - def cleanup(self): - """Cleanup the widget connection from BECDispatcher.""" - self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment()) - self.bec_dispatcher.disconnect_slot( - self.update_dap, MessageEndpoints.dap_response(self.scan_id) - ) - for curve_id in self._curves_data["async"]: - self.bec_dispatcher.disconnect_slot( - self.on_async_readback, - MessageEndpoints.device_async_readback(self.scan_id, curve_id), - ) - self.curves.clear() diff --git a/bec_widgets/widgets/containers/figure/plots/waveform/waveform_curve.py b/bec_widgets/widgets/containers/figure/plots/waveform/waveform_curve.py deleted file mode 100644 index 2e0bbd50..00000000 --- a/bec_widgets/widgets/containers/figure/plots/waveform/waveform_curve.py +++ /dev/null @@ -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() diff --git a/tests/end-2-end/conftest.py b/tests/end-2-end/conftest.py index cdec5764..518c979f 100644 --- a/tests/end-2-end/conftest.py +++ b/tests/end-2-end/conftest.py @@ -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)}" diff --git a/tests/end-2-end/test_bec_figure_rpc_e2e.py b/tests/end-2-end/test_bec_figure_rpc_e2e.py deleted file mode 100644 index 7f6f1588..00000000 --- a/tests/end-2-end/test_bec_figure_rpc_e2e.py +++ /dev/null @@ -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 diff --git a/tests/unit_tests/test_bec_dock.py b/tests/unit_tests/test_bec_dock.py index 9ecfa00c..7922a86e 100644 --- a/tests/unit_tests/test_bec_dock.py +++ b/tests/unit_tests/test_bec_dock.py @@ -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") diff --git a/tests/unit_tests/test_bec_figure.py b/tests/unit_tests/test_bec_figure.py deleted file mode 100644 index 61f73a10..00000000 --- a/tests/unit_tests/test_bec_figure.py +++ /dev/null @@ -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 diff --git a/tests/unit_tests/test_bec_image.py b/tests/unit_tests/test_bec_image.py deleted file mode 100644 index 44e0b15a..00000000 --- a/tests/unit_tests/test_bec_image.py +++ /dev/null @@ -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" diff --git a/tests/unit_tests/test_bec_motor_map.py b/tests/unit_tests/test_bec_motor_map.py deleted file mode 100644 index f304937e..00000000 --- a/tests/unit_tests/test_bec_motor_map.py +++ /dev/null @@ -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:] diff --git a/tests/unit_tests/test_multi_waveform.py b/tests/unit_tests/test_multi_waveform.py deleted file mode 100644 index 2de43f9d..00000000 --- a/tests/unit_tests/test_multi_waveform.py +++ /dev/null @@ -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() diff --git a/tests/unit_tests/test_plot_base.py b/tests/unit_tests/test_plot_base.py deleted file mode 100644 index d6a9b32b..00000000 --- a/tests/unit_tests/test_plot_base.py +++ /dev/null @@ -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 diff --git a/tests/unit_tests/test_plugin_utils.py b/tests/unit_tests/test_plugin_utils.py index d8397ec4..5650b531 100644 --- a/tests/unit_tests/test_plugin_utils.py +++ b/tests/unit_tests/test_plugin_utils.py @@ -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 diff --git a/tests/unit_tests/test_rpc_server.py b/tests/unit_tests/test_rpc_server.py index f3e0aa9f..5056948a 100644 --- a/tests/unit_tests/test_rpc_server.py +++ b/tests/unit_tests/test_rpc_server.py @@ -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" ) diff --git a/tests/unit_tests/test_utils_plot_indicators.py b/tests/unit_tests/test_utils_plot_indicators.py index 70fcd360..9c3037e5 100644 --- a/tests/unit_tests/test_utils_plot_indicators.py +++ b/tests/unit_tests/test_utils_plot_indicators.py @@ -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 diff --git a/tests/unit_tests/test_waveform1d.py b/tests/unit_tests/test_waveform1d.py deleted file mode 100644 index e70793ff..00000000 --- a/tests/unit_tests/test_waveform1d.py +++ /dev/null @@ -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]"