1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-08 09:47:52 +02:00

Compare commits

..

18 Commits

Author SHA1 Message Date
semantic-release
ee02c13d5d 0.44.2
Automatically generated by python-semantic-release
2024-03-20 11:50:29 +00:00
wyzula-jan
9ccd4ea235 fix(utils/bec_dispatcher): try/except to start client, to avoid crash when redis is not running 2024-03-20 12:06:22 +01:00
wyzula-jan
86416d50cb fix(utils/bec_dispatcher): bec_dispatcher adjusted to the new BECClient; dropped support to inject bec_config.yaml, instead BECClient can be passed as arg 2024-03-20 11:12:16 +01:00
1d5442ac08 ci: now testing against master branches of bec_lib and ophyd_devices 2024-03-20 11:11:49 +01:00
semantic-release
f3c7196921 0.44.1
Automatically generated by python-semantic-release
2024-03-19 09:05:40 +00:00
wyzula-jan
14f901f1be fix(examples/motor_compilation): motor_control_compilations.py do not have any hardcoded config anymore 2024-03-18 18:18:48 +01:00
semantic-release
9f93c01ff7 0.44.0
Automatically generated by python-semantic-release
2024-03-18 08:05:04 +00:00
203ae09606 fix(cli): removed hard-coded signal 2024-03-18 07:43:32 +01:00
2d39c5e4d1 fix(cli): fixed cleanup procedure 2024-03-18 07:43:32 +01:00
9049e0d27f feat(cli): added update script to BECFigure 2024-03-18 07:43:32 +01:00
semantic-release
d5d41fc759 0.43.2
Automatically generated by python-semantic-release
2024-03-18 06:38:10 +00:00
wyzula-jan
d0f9bf1733 fix(cli/server): added QApplications to enter separate QT event loop ensuring that QT objects are not deleted 2024-03-17 18:22:21 +01:00
semantic-release
7d46d1160d 0.43.1
Automatically generated by python-semantic-release
2024-03-15 16:22:33 +00:00
wyzula-jan
b8d4e697ac fix(plots/image): same access pattern for image and image_item for setting up parameters, autorange of z scale disabled by default 2024-03-15 16:44:32 +01:00
wyzula-jan
4664661cfb fix(widget/various): corrected USER_ACCESS methods for children widgets to include inherited methods to RPC 2024-03-15 16:44:32 +01:00
wyzula-jan
d99fd76c0b refactor(widget/figure): changed add_plot and add_image to specify what should be content of the widget, instead of widget id 2024-03-15 16:44:32 +01:00
wyzula-jan
fcf918c488 fix(widgets/figure): added widgets can be accessed as a list (fig.axes) or as a dictionary (fig.widgets) 2024-03-15 16:44:32 +01:00
wyzula-jan
32747baa27 refactor(cli): commented debug CLI messages 2024-03-14 15:17:11 +01:00
17 changed files with 865 additions and 168 deletions

View File

@@ -5,6 +5,8 @@ image: $CI_DOCKER_REGISTRY/python:3.10
#commands to run in the Docker container before starting each job.
variables:
DOCKER_TLS_CERTDIR: ""
BEC_CORE_BRANCH: "master"
OPHYD_DEVICES_BRANCH: "master"
include:
- template: Security/Secret-Detection.gitlab-ci.yml
@@ -77,9 +79,13 @@ tests:
variables:
QT_QPA_PLATFORM: "offscreen"
script:
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install .[dev]
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
- coverage report
- coverage xml

View File

@@ -2,6 +2,44 @@
<!--next-version-placeholder-->
## v0.44.2 (2024-03-20)
### Fix
* **utils/bec_dispatcher:** Try/except to start client, to avoid crash when redis is not running ([`9ccd4ea`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/9ccd4ea235be4c4332045b7a7f09d6cc6291f7ff))
* **utils/bec_dispatcher:** Bec_dispatcher adjusted to the new BECClient; dropped support to inject bec_config.yaml, instead BECClient can be passed as arg ([`86416d5`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/86416d50cb850b42d312fe17fc46f0b4743dc940))
## v0.44.1 (2024-03-19)
### Fix
* **examples/motor_compilation:** Motor_control_compilations.py do not have any hardcoded config anymore ([`14f901f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/14f901f1bea2ba7b79903c4743e37384e11533d3))
## v0.44.0 (2024-03-18)
### Feature
* **cli:** Added update script to BECFigure ([`9049e0d`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/9049e0d27fe9a3860e21ffc3b350eb69e567b71c))
### Fix
* **cli:** Removed hard-coded signal ([`203ae09`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/203ae0960688608fb609a742a23e5994bfe9805c))
* **cli:** Fixed cleanup procedure ([`2d39c5e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/2d39c5e4d18bbb66a5f3340fce7f8944dd4ba19f))
## v0.43.2 (2024-03-18)
### Fix
* **cli/server:** Added QApplications to enter separate QT event loop ensuring that QT objects are not deleted ([`d0f9bf1`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/d0f9bf17339296a60301e5e6ffe602db369c6c7c))
## v0.43.1 (2024-03-15)
### Fix
* **plots/image:** Same access pattern for image and image_item for setting up parameters, autorange of z scale disabled by default ([`b8d4e69`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b8d4e697ac2a5929a1374ce1778046efc3f8187a))
* **widget/various:** Corrected USER_ACCESS methods for children widgets to include inherited methods to RPC ([`4664661`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/4664661cfb4e8bd4a6adb71f2050b25d0b4f3d36))
* **widgets/figure:** Added widgets can be accessed as a list (fig.axes) or as a dictionary (fig.widgets) ([`fcf918c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/fcf918c48862d069b9fe69cbba7dbecbe7429790))
## v0.43.0 (2024-03-14)
### Feature

View File

@@ -0,0 +1 @@
from .client import BECFigure

View File

@@ -5,6 +5,16 @@ from typing import Literal, Optional, overload
class BECPlotBase(RPCBase):
@rpc_call
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""
@rpc_call
def set(self, **kwargs) -> "None":
"""
@@ -241,12 +251,165 @@ class BECWaveform1D(RPCBase):
dict | pd.DataFrame: Data of all curves in the specified format.
"""
@rpc_call
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""
@rpc_call
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
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the plot widget.
Args:
title(str): Title of the plot widget.
"""
@rpc_call
def set_x_label(self, label: "str"):
"""
Set the label of the x-axis.
Args:
label(str): Label of the x-axis.
"""
@rpc_call
def set_y_label(self, label: "str"):
"""
Set the label of the y-axis.
Args:
label(str): Label of the y-axis.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
Lock aspect ratio.
Args:
lock(bool): True to lock, False to unlock.
"""
@rpc_call
def plot(self, data_x: "list | np.ndarray", data_y: "list | np.ndarray", **kwargs):
"""
Plot custom data on the plot widget. These data are not saved in config.
Args:
data_x(list|np.ndarray): x-axis data
data_y(list|np.ndarray): y-axis data
**kwargs: Keyword arguments for the plot.
"""
@rpc_call
def remove(self):
"""
Remove the plot widget from the figure.
"""
class BECFigure(RPCBase, BECFigureClientMixin):
@property
@rpc_call
def axes(self) -> "list[BECPlotBase]":
"""
Access all widget in BECFigure as a list
Returns:
list[BECPlotBase]: List of all widgets in the figure.
"""
@property
@rpc_call
def widgets(self) -> "dict":
"""
None
"""
@rpc_call
def add_plot(
self,
widget_id: "str" = None,
x_name: "str" = None,
y_name: "str" = None,
x_entry: "str" = None,
y_entry: "str" = None,
x: "list | np.ndarray" = None,
y: "list | np.ndarray" = None,
color: "Optional[str]" = None,
label: "Optional[str]" = None,
validate: "bool" = True,
row: "int" = None,
col: "int" = None,
config=None,
@@ -265,17 +428,31 @@ class BECFigure(RPCBase, BECFigureClientMixin):
@rpc_call
def add_image(
self,
widget_id: "str" = None,
monitor: "str" = None,
color_bar: "Literal['simple', 'full']" = "full",
color_map: "str" = "magma",
data: "np.ndarray" = None,
vrange: "tuple[float, float]" = None,
row: "int" = None,
col: "int" = None,
config=None,
color_map: "str" = "magma",
color_bar: "Literal['simple', 'full']" = "full",
vrange: "tuple[float, float]" = None,
**axis_kwargs
) -> "BECImageShow":
"""
None
Add an image to the figure at the specified position.
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.
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:
Returns:
BECImageShow: The image widget.
"""
@rpc_call
@@ -293,7 +470,7 @@ class BECFigure(RPCBase, BECFigureClientMixin):
**axis_kwargs
) -> "BECWaveform1D":
"""
Add a 1D waveform plot to the figure.
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
Args:
x_name(str): The name of the device for the x-axis.
y_name(str): The name of the device for the y-axis.
@@ -321,7 +498,7 @@ class BECFigure(RPCBase, BECFigureClientMixin):
**axis_kwargs
) -> "BECImageShow":
"""
Add an image to the figure.
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.
@@ -559,6 +736,236 @@ class BECImageShow(RPCBase):
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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
"""
@rpc_call
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
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
def toggle_threading(self, use_threading: "bool"):
"""
Toggle threading for the widgets postprocessing and updating.
Args:
use_threading(bool): Whether to use threading.
"""
@rpc_call
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""
@rpc_call
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
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the plot widget.
Args:
title(str): Title of the plot widget.
"""
@rpc_call
def set_x_label(self, label: "str"):
"""
Set the label of the x-axis.
Args:
label(str): Label of the x-axis.
"""
@rpc_call
def set_y_label(self, label: "str"):
"""
Set the label of the y-axis.
Args:
label(str): Label of the y-axis.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
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.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
Lock aspect ratio.
Args:
lock(bool): True to lock, False to unlock.
"""
@rpc_call
def plot(self, data_x: "list | np.ndarray", data_y: "list | np.ndarray", **kwargs):
"""
Plot custom data on the plot widget. These data are not saved in config.
Args:
data_x(list|np.ndarray): x-axis data
data_y(list|np.ndarray): y-axis data
**kwargs: Keyword arguments for the plot.
"""
@rpc_call
def remove(self):
"""
Remove the plot widget from the figure.
"""
class BECConnector(RPCBase):
@rpc_call
@@ -632,7 +1039,7 @@ class BECImageItem(RPCBase):
"""
@rpc_call
def set_autorange(self, autorange: "bool" = True):
def set_autorange(self, autorange: "bool" = False):
"""
Set the autorange of the color bar.
Args:
@@ -673,3 +1080,13 @@ class BECImageItem(RPCBase):
vmin(float): Minimum value of the color bar.
vmax(float): Maximum value of the color bar.
"""
@rpc_call
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""

View File

@@ -1,16 +1,23 @@
from __future__ import annotations
import importlib
import select
import subprocess
import uuid
from functools import wraps
from typing import TYPE_CHECKING
from bec_lib import MessageEndpoints, messages
from bec_lib.connector import MessageObject
from bec_lib.device import DeviceBase
from qtpy.QtCore import QCoreApplication
import bec_widgets.cli.client as client
from bec_lib import MessageEndpoints, messages
from bec_widgets.utils.bec_dispatcher import BECDispatcher
if TYPE_CHECKING:
from bec_widgets.cli.client import BECFigure
def rpc_call(func):
"""
@@ -30,10 +37,97 @@ def rpc_call(func):
return wrapper
def get_selected_device(monitored_devices, selected_device):
"""
Get the selected device for the plot. If no device is selected, the first
device in the monitored devices list is selected.
"""
if selected_device:
return selected_device
if len(monitored_devices) > 0:
sel_device = monitored_devices[0]
return sel_device
return None
def update_script(figure: BECFigure, msg):
"""
Update the script with the given data.
"""
info = msg.info
status = msg.status
scan_id = msg.scanID
scan_number = info.get("scan_number", 0)
scan_name = info.get("scan_name", "Unknown")
scan_report_devices = info.get("scan_report_devices", [])
monitored_devices = info.get("readout_priority", {}).get("monitored", [])
monitored_devices = [dev for dev in monitored_devices if dev not in scan_report_devices]
if scan_name == "line_scan" and scan_report_devices:
dev_x = scan_report_devices[0]
dev_y = get_selected_device(monitored_devices, figure.selected_device)
print(f"Selected device: {dev_y}")
if not dev_y:
return
figure.clear_all()
plt = figure.plot(dev_x, dev_y)
plt.set(title=f"Scan {scan_number}", x_label=dev_x, y_label=dev_y)
elif scan_name == "grid_scan" and scan_report_devices:
print(f"Scan {scan_number} is running")
dev_x = scan_report_devices[0]
dev_y = scan_report_devices[1]
figure.clear_all()
plt = figure.plot(dev_x, dev_y, label=f"Scan {scan_number}")
plt.set(title=f"Scan {scan_number}", x_label=dev_x, y_label=dev_y)
elif scan_report_devices:
dev_x = scan_report_devices[0]
dev_y = get_selected_device(monitored_devices, figure.selected_device)
if not dev_y:
return
figure.clear_all()
plt = figure.plot(dev_x, dev_y, label=f"Scan {scan_number}")
plt.set(title=f"Scan {scan_number}", x_label=dev_x, y_label=dev_y)
class BECFigureClientMixin:
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._process = None
self.update_script = update_script
self._target_endpoint = MessageEndpoints.scan_status()
self._selected_device = None
@property
def selected_device(self):
"""
Selected device for the plot.
"""
return self._selected_device
@selected_device.setter
def selected_device(self, device: str | DeviceBase):
if isinstance(device, DeviceBase):
self._selected_device = device.name
elif isinstance(device, str):
self._selected_device = device
else:
raise ValueError("Device must be a string or a device object")
def _start_update_script(self) -> None:
self._client.connector.register(
self._target_endpoint, cb=self._handle_msg_update, parent=self
)
@staticmethod
def _handle_msg_update(msg: MessageObject, parent: BECFigureClientMixin) -> None:
if parent.update_script is not None:
# pylint: disable=protected-access
parent._update_script_msg_parser(msg.value)
def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
if isinstance(msg, messages.ScanStatusMessage):
if msg.status == "open":
self.update_script(self, msg)
def show(self) -> None:
"""
@@ -56,6 +150,7 @@ class BECFigureClientMixin:
"""
Start the plot in a new process.
"""
self._start_update_script()
# pylint: disable=subprocess-run-check
monitor_module = importlib.import_module("bec_widgets.cli.server")
monitor_path = monitor_module.__file__
@@ -85,9 +180,6 @@ class BECFigureClientMixin:
stderr_output.append(line.decode("utf-8"))
return "".join(stderr_output)
def __del__(self) -> None:
self.close()
class RPCBase:
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
@@ -96,7 +188,12 @@ class RPCBase:
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())
self._parent = parent
super().__init__()
print(f"RPCBase: {self._gui_id}")
# print(f"RPCBase: {self._gui_id}")
def __repr__(self):
type_ = type(self)
qualname = type_.__qualname__
return f"<{qualname} object at {hex(id(self))}>"
@property
def _root(self):
@@ -129,7 +226,7 @@ class RPCBase:
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
metadata={"request_id": request_id},
)
print(f"RPCBase: {rpc_msg}")
# print(f"RPCBase: {rpc_msg}")
# pylint: disable=protected-access
receiver = self._root._gui_id
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
@@ -150,7 +247,9 @@ class RPCBase:
return [self._create_widget_from_msg_result(res) for res in msg_result]
if isinstance(msg_result, dict):
if "__rpc__" not in msg_result:
return msg_result
return {
key: self._create_widget_from_msg_result(val) for key, val in msg_result.items()
}
cls = msg_result.pop("widget_class", None)
msg_result.pop("__rpc__", None)
@@ -158,7 +257,7 @@ class RPCBase:
return msg_result
cls = getattr(client, cls)
print(msg_result)
# print(msg_result)
return cls(parent=self, **msg_result)
return msg_result

View File

@@ -17,7 +17,7 @@ class BECWidgetsCLIServer:
self.client.start()
self.gui_id = gui_id
self.fig = BECFigure(gui_id=self.gui_id)
print(f"Server started with gui_id {self.gui_id}")
# print(f"Server started with gui_id {self.gui_id}")
self.dispatcher.connect_slot(
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
@@ -54,17 +54,14 @@ class BECWidgetsCLIServer:
if gui_id == self.fig.gui_id:
return self.fig
# check if the object is a widget
if gui_id in self.fig.widgets:
obj = self.fig.widgets[config["gui_id"]]
if gui_id in self.fig._widgets:
obj = self.fig._widgets[config["gui_id"]]
return obj
if self.fig.widgets:
for widget in self.fig.widgets.values():
if self.fig._widgets:
for widget in self.fig._widgets.values():
item = widget.find_widget_by_id(gui_id)
if item:
return item
# raise NotImplementedError(
# f"gui_id lookup for widget of type {widget.__class__.__name__} not implemented"
# )
raise ValueError(f"Object with gui_id {gui_id} not found")
@@ -82,6 +79,8 @@ class BECWidgetsCLIServer:
if isinstance(res, list):
res = [self.serialize_object(obj) for obj in res]
elif isinstance(res, dict):
res = {key: self.serialize_object(val) for key, val in res.items()}
else:
res = self.serialize_object(res)
return res
@@ -99,6 +98,10 @@ class BECWidgetsCLIServer:
if __name__ == "__main__": # pragma: no cover
import argparse
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
parser.add_argument("--id", type=str, help="The id of the server")
@@ -108,3 +111,4 @@ if __name__ == "__main__": # pragma: no cover
server = BECWidgetsCLIServer(gui_id=args.id)
# server = BECWidgetsCLIServer(gui_id="test")
server.start()
sys.exit(app.exec())

View File

@@ -242,19 +242,19 @@ if __name__ == "__main__": # pragma: no cover
qdarktheme.setup_theme("auto")
if args.variant == "app":
window = MotorControlApp(client=client, config=CONFIG_DEFAULT)
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "map":
window = MotorControlMap(client=client, config=CONFIG_DEFAULT)
window = MotorControlMap(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel":
window = MotorControlPanel(client=client, config=CONFIG_DEFAULT)
window = MotorControlPanel(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_abs":
window = MotorControlPanelAbsolute(client=client, config=CONFIG_DEFAULT)
window = MotorControlPanelAbsolute(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_rel":
window = MotorControlPanelRelative(client=client, config=CONFIG_DEFAULT)
window = MotorControlPanelRelative(client=client) # , config=CONFIG_DEFAULT)
else:
print("Please specify a valid variant to run. Use -h for help.")
print("Running the full application by default.")
window = MotorControlApp(client=client, config=CONFIG_DEFAULT)
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
window.show()
sys.exit(app.exec())

View File

@@ -35,19 +35,14 @@ class _Connection:
class _BECDispatcher(QObject):
"""Utility class to keep track of slots connected to a particular redis connector"""
def __init__(self, bec_config=None):
def __init__(self, client=None):
super().__init__()
self.client = BECClient()
# TODO: this is a workaround for now to provide service config within qtdesigner, but is
# it possible to provide config via a cli arg?
if bec_config is None and os.path.isfile("bec_config.yaml"):
bec_config = "bec_config.yaml"
self.client = BECClient() if client is None else client
try:
self.client.initialize(config=ServiceConfig(config_path=bec_config))
except redis.exceptions.ConnectionError as e:
print(f"Failed to initialize BECClient: {e}")
self.client.start()
except redis.exceptions.ConnectionError:
print("Could not connect to Redis, skipping start of BECClient.")
self._connections = {}
def connect_slot(
@@ -186,8 +181,8 @@ def BECDispatcher():
global _bec_dispatcher
if _bec_dispatcher is None:
parser = argparse.ArgumentParser()
parser.add_argument("--bec-config", default=None)
parser.add_argument("--bec-client", default=None)
args, _ = parser.parse_known_args()
_bec_dispatcher = _BECDispatcher(args.bec_config)
_bec_dispatcher = _BECDispatcher(args.bec_client)
return _bec_dispatcher

View File

@@ -95,6 +95,8 @@ class WidgetHandler:
class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
USER_ACCESS = [
"axes",
"widgets",
"add_plot",
"add_image",
"plot",
@@ -127,13 +129,48 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
self.widget_handler = WidgetHandler()
# Widget container to reference widgets by 'widget_id'
self.widgets = defaultdict(dict)
self._widgets = defaultdict(dict)
# Container to keep track of the grid
self.grid = []
@property
def axes(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
@axes.setter
def axes(self, value: list[BECPlotBase]):
self._axes = value
@property
def widgets(self) -> dict:
return self._widgets
@widgets.setter
def widgets(self, value: dict):
self._widgets = value
def add_plot(
self, widget_id: str = None, row: int = None, col: int = None, config=None, **axis_kwargs
self,
x_name: str = None,
y_name: str = None,
x_entry: str = None,
y_entry: str = None,
x: list | np.ndarray = None,
y: list | np.ndarray = None,
color: Optional[str] = None,
label: Optional[str] = None,
validate: bool = True,
row: int = None,
col: int = None,
config=None,
**axis_kwargs,
) -> BECWaveform1D:
"""
Add a Waveform1D plot to the figure at the specified position.
@@ -144,7 +181,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
config(dict): Additional configuration for the widget.
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
"""
return self.add_widget(
widget_id = self._generate_unique_widget_id()
waveform = self.add_widget(
widget_type="Waveform1D",
widget_id=widget_id,
row=row,
@@ -153,6 +191,30 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
**axis_kwargs,
)
# TODO remove repetition from .plot method
# User wants to add scan curve
if x_name is not None and y_name is not None and x is None and y is None:
waveform.add_curve_scan(
x_name=x_name,
y_name=y_name,
x_entry=x_entry,
y_entry=y_entry,
validate=validate,
color=color,
label=label,
)
# User wants to add custom curve
elif x is not None and y is not None and x_name is None and y_name is None:
waveform.add_curve_custom(
x=x,
y=y,
color=color,
label=label,
)
return waveform
def plot(
self,
x_name: str = None,
@@ -167,7 +229,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
**axis_kwargs,
) -> BECWaveform1D:
"""
Add a 1D waveform plot to the figure.
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
Args:
x_name(str): The name of the device for the x-axis.
y_name(str): The name of the device for the y-axis.
@@ -225,7 +287,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
**axis_kwargs,
) -> BECImageShow:
"""
Add an image to the figure.
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.
@@ -265,17 +327,35 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
def add_image(
self,
widget_id: str = None,
monitor: str = None,
color_bar: Literal["simple", "full"] = "full",
color_map: str = "magma",
data: np.ndarray = None,
vrange: tuple[float, float] = None,
row: int = None,
col: int = None,
config=None,
color_map: str = "magma", # TODO fix passing additional kwargs
color_bar: Literal["simple", "full"] = "full",
vrange: tuple[float, float] = None,
**axis_kwargs,
) -> BECImageShow:
"""
Add an image to the figure at the specified position.
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.
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:
Returns:
BECImageShow: The image widget.
"""
widget_id = self._generate_unique_widget_id()
if config is None:
widget_id = self._generate_unique_widget_id()
config = ImageConfig(
widget_class="BECImageShow",
gui_id=widget_id,
@@ -284,7 +364,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_bar=color_bar,
vrange=vrange,
)
return self.add_widget(
image = self.add_widget(
widget_type="ImShow",
widget_id=widget_id,
row=row,
@@ -292,6 +372,23 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
config=config,
**axis_kwargs,
)
# TODO remove repetition from .image method
if monitor is not None and data is None:
image.add_monitor_image(
monitor=monitor, 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)
return image
def add_widget(
self,
@@ -314,7 +411,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
"""
if not widget_id:
widget_id = self._generate_unique_widget_id()
if widget_id in self.widgets:
if widget_id in self._widgets:
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
widget = self.widget_handler.create_widget(
@@ -350,7 +447,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
# Saving config for future referencing
self.config.widgets[widget_id] = widget.config
self.widgets[widget_id] = widget
self._widgets[widget_id] = widget
# Reflect the grid coordinates
self._change_grid(widget_id, row, col)
@@ -402,7 +499,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
Returns:
BECPlotBase: The widget of the given class.
"""
for widget_id, widget in self.widgets.items():
for widget_id, widget in self._widgets.items():
if isinstance(widget, widget_class):
return widget
if can_fail:
@@ -420,7 +517,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
widget = self._get_widget_by_coordinates(row, col)
if widget:
widget_id = widget.config.gui_id
if widget_id in self.widgets:
if widget_id in self._widgets:
self._remove_by_id(widget_id)
def _remove_by_id(self, widget_id: str) -> None:
@@ -429,8 +526,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
Args:
widget_id(str): The unique identifier of the widget to remove.
"""
if widget_id in self.widgets:
widget = self.widgets.pop(widget_id)
if widget_id in self._widgets:
widget = self._widgets.pop(widget_id)
widget.cleanup()
self.removeItem(widget)
self.grid[widget.config.row][widget.config.col] = None
@@ -445,10 +542,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
if isinstance(key, tuple) and len(key) == 2:
return self._get_widget_by_coordinates(*key)
elif isinstance(key, str):
widget = self.widgets.get(key)
widget = self._widgets.get(key)
if widget is None:
raise KeyError(f"No widget with ID {key}")
return self.widgets.get(key)
return self._widgets.get(key)
else:
raise TypeError(
"Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
@@ -478,7 +575,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
def _generate_unique_widget_id(self):
"""Generate a unique widget ID."""
existing_ids = set(self.widgets.keys())
existing_ids = set(self._widgets.keys())
for i in itertools.count(1):
widget_id = f"widget_{i}"
if widget_id not in existing_ids:
@@ -511,7 +608,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
# 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._widgets[widget].config.row, self._widgets[widget].config.col = (
row_idx,
col_idx,
)
self.grid = new_grid
self._replot_layout()
@@ -521,7 +621,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
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)
self.addItem(self._widgets[widget], row=row_idx, col=col_idx)
def change_layout(self, max_columns=None, max_rows=None):
"""
@@ -533,7 +633,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
max_rows (Optional[int]): The new maximum number of rows in the figure.
"""
# Calculate total number of widgets
total_widgets = len(self.widgets)
total_widgets = len(self._widgets)
if max_columns:
# Calculate the required number of rows based on max_columns
@@ -549,7 +649,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
# Populate the new grid with widgets' IDs
current_idx = 0
for widget_id, widget in self.widgets.items():
for widget_id, widget in self._widgets.items():
row = current_idx // len(new_grid[0])
col = current_idx % len(new_grid[0])
new_grid[row][col] = widget_id
@@ -565,10 +665,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
def clear_all(self):
"""Clear all widgets from the figure and reset to default state"""
for widget in self.widgets.values():
for widget in self._widgets.values():
widget.cleanup()
self.clear()
self.widgets = defaultdict(dict)
self._widgets = defaultdict(dict)
self.grid = []
theme = self.config.theme
self.config = FigureConfig(

View File

@@ -45,6 +45,9 @@ class MotorControlWidget(QWidget):
self.motor_thread = motor_thread
self.config = config
self.motor_x = None
self.motor_y = None
if not self.client:
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client

View File

@@ -69,6 +69,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
"set_auto_downsample",
"set_monitor",
"set_vrange",
"get_config",
]
def __init__(
@@ -181,7 +182,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
self.setOpacity(opacity)
self.config.opacity = opacity
def set_autorange(self, autorange: bool = True):
def set_autorange(self, autorange: bool = False):
"""
Set the autorange of the color bar.
Args:
@@ -287,6 +288,28 @@ class BECImageShow(BECPlotBase):
"add_custom_image",
"set_vrange",
"set_color_map",
"set_autorange",
"set_monitor",
"set_processing",
"set_image_properties",
"set_fft",
"set_log",
"set_rotation",
"set_transpose",
"toggle_threading",
"get_config",
"set",
"set_title",
"set_x_label",
"set_y_label",
"set_x_scale",
"set_y_scale",
"set_x_lim",
"set_y_lim",
"set_grid",
"lock_aspect_ratio",
"plot",
"remove",
]
def __init__(
@@ -555,6 +578,25 @@ class BECImageShow(BECPlotBase):
"""
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_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.

View File

@@ -38,6 +38,7 @@ class WidgetConfig(ConnectionConfig):
class BECPlotBase(BECConnector, pg.GraphicsLayout):
USER_ACCESS = [
"get_config",
"set",
"set_title",
"set_x_label",

View File

@@ -219,11 +219,23 @@ class BECWaveform1D(BECPlotBase):
"remove_curve",
"scan_history",
"curves",
# "curves_data",
"get_curve",
"get_curve_config",
"apply_config",
"get_all_data",
"get_config",
"set",
"set_title",
"set_x_label",
"set_y_label",
"set_x_scale",
"set_y_scale",
"set_x_lim",
"set_y_lim",
"set_grid",
"lock_aspect_ratio",
"plot",
"remove",
]
scan_signal_update = pyqtSignal()
@@ -356,19 +368,6 @@ class BECWaveform1D(BECPlotBase):
def curves(self, value: list[BECCurve]):
self._curves = value
@property
def curves_data(self) -> dict[str, dict[str, BECCurve]]:
"""
Get the curves data of the plot widget as a dictionary
Returns:
dict: Dictionary of curves data.
"""
return self._curves_data
@curves_data.setter
def curves_data(self, value: dict[str, dict[str, BECCurve]]):
self._curves_data = value
def get_curve(self, identifier) -> BECCurve:
"""
Get the curve by its index or ID.

View File

@@ -1,7 +1,7 @@
# pylint: disable= missing-module-docstring
from setuptools import setup, find_packages
__version__ = "0.43.0"
__version__ = "0.44.2"
# Default to PyQt6 if no other Qt binding is installed
QT_DEPENDENCY = "PyQt6>=6.0"

View File

@@ -30,6 +30,6 @@ def bec_dispatcher(threads_check):
yield bec_dispatcher
bec_dispatcher.disconnect_all()
# clean BEC client
BECService.shutdown(bec_dispatcher.client)
bec_dispatcher.client.shutdown()
# reinitialize singleton for next test
bec_dispatcher_module._bec_dispatcher = None

View File

@@ -36,37 +36,37 @@ def test_bec_figure_init_with_config(mocked_client):
def test_bec_figure_add_remove_plot(bec_figure):
initial_count = len(bec_figure.widgets)
initial_count = len(bec_figure._widgets)
# Adding 3 widgets - 2 WaveformBase and 1 PlotBase
w0 = bec_figure.add_plot()
w1 = bec_figure.add_plot(widget_id="test_waveform")
w2 = bec_figure.add_widget(widget_id="test_plot", widget_type="PlotBase")
w1 = bec_figure.add_plot()
w2 = bec_figure.add_widget(widget_type="PlotBase")
# Check if the widgets were added
assert len(bec_figure.widgets) == initial_count + 3
assert "widget_1" in bec_figure.widgets
assert "test_plot" in bec_figure.widgets
assert "test_waveform" in bec_figure.widgets
assert bec_figure.widgets["widget_1"].config.widget_class == "BECWaveform1D"
assert bec_figure.widgets["test_plot"].config.widget_class == "BECPlotBase"
assert bec_figure.widgets["test_waveform"].config.widget_class == "BECWaveform1D"
assert len(bec_figure._widgets) == initial_count + 3
assert "widget_1" in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert "widget_3" in bec_figure._widgets
assert bec_figure._widgets["widget_1"].config.widget_class == "BECWaveform1D"
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform1D"
assert bec_figure._widgets["widget_3"].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 - PlotBase
bec_figure.remove(widget_id="test_plot")
assert len(bec_figure.widgets) == initial_count + 2
assert "test_plot" not in bec_figure.widgets
assert "test_waveform" in bec_figure.widgets
assert bec_figure.widgets["test_waveform"].config.widget_class == "BECWaveform1D"
# Removing 1 widget
bec_figure.remove(widget_id="widget_1")
assert len(bec_figure._widgets) == initial_count + 2
assert "widget_1" not in bec_figure._widgets
assert "widget_3" in bec_figure._widgets
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform1D"
def test_access_widgets_access_errors(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
# access widget by non-existent coordinates
with pytest.raises(ValueError) as excinfo:
@@ -88,26 +88,18 @@ def test_access_widgets_access_errors(bec_figure):
def test_add_plot_to_occupied_position(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
assert "Position at row 0 and column 0 is already occupied." in str(excinfo.value)
def test_add_plot_to_occupied_id(bec_figure):
bec_figure.add_plot(widget_id="test_waveform", row=0, col=0)
with pytest.raises(ValueError) as excinfo:
bec_figure.add_plot(widget_id="test_waveform", row=0, col=1)
assert "Widget with ID 'test_waveform' already exists" in str(excinfo.value)
def test_remove_plots(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
w2 = bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=1)
w3 = bec_figure.add_plot(widget_id="test_waveform_3", row=1, col=0)
w4 = bec_figure.add_plot(widget_id="test_waveform_4", row=1, col=1)
w1 = bec_figure.add_plot(row=0, col=0)
w2 = bec_figure.add_plot(row=0, col=1)
w3 = bec_figure.add_plot(row=1, col=0)
w4 = bec_figure.add_plot(row=1, col=1)
assert bec_figure[0, 0] == w1
assert bec_figure[0, 1] == w2
@@ -116,47 +108,47 @@ def test_remove_plots(bec_figure):
# remove by coordinates
bec_figure[0, 0].remove()
assert "test_waveform_1" not in bec_figure.widgets
assert "widget_1" not in bec_figure._widgets
# remove by widget_id
bec_figure.remove(widget_id="test_waveform_2")
assert "test_waveform_2" not in bec_figure.widgets
bec_figure.remove(widget_id="widget_2")
assert "widget_2" not in bec_figure._widgets
# remove by widget object
w3.remove()
assert "test_waveform_3" not in bec_figure.widgets
assert "widget_3" not in bec_figure._widgets
# check the remaining widget 4
assert bec_figure[0, 0] == w4
assert bec_figure["test_waveform_4"] == w4
assert "test_waveform_4" in bec_figure.widgets
assert len(bec_figure.widgets) == 1
assert bec_figure["widget_4"] == w4
assert "widget_4" in bec_figure._widgets
assert len(bec_figure._widgets) == 1
def test_remove_plots_by_coordinates_ints(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
w2 = bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=1)
w1 = bec_figure.add_plot(row=0, col=0)
w2 = bec_figure.add_plot(row=0, col=1)
bec_figure.remove(0, 0)
assert "test_waveform_1" not in bec_figure.widgets
assert "test_waveform_2" in bec_figure.widgets
assert "widget_1" not in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert bec_figure[0, 0] == w2
assert len(bec_figure.widgets) == 1
assert len(bec_figure._widgets) == 1
def test_remove_plots_by_coordinates_tuple(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
w2 = bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=1)
w1 = bec_figure.add_plot(row=0, col=0)
w2 = bec_figure.add_plot(row=0, col=1)
bec_figure.remove(coordinates=(0, 0))
assert "test_waveform_1" not in bec_figure.widgets
assert "test_waveform_2" in bec_figure.widgets
assert "widget_1" not in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert bec_figure[0, 0] == w2
assert len(bec_figure.widgets) == 1
assert len(bec_figure._widgets) == 1
def test_remove_plot_by_id_error(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
bec_figure.remove(widget_id="non_existent_widget")
@@ -164,7 +156,7 @@ def test_remove_plot_by_id_error(bec_figure):
def test_remove_plot_by_coordinates_error(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
bec_figure.remove(0, 1)
@@ -172,7 +164,7 @@ def test_remove_plot_by_coordinates_error(bec_figure):
def test_remove_plot_by_providing_nothing(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(row=0, col=0)
with pytest.raises(ValueError) as excinfo:
bec_figure.remove()
@@ -192,10 +184,10 @@ def test_remove_plot_by_providing_nothing(bec_figure):
def test_change_layout(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
w2 = bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=1)
w3 = bec_figure.add_plot(widget_id="test_waveform_3", row=1, col=0)
w4 = bec_figure.add_plot(widget_id="test_waveform_4", row=1, col=1)
w1 = bec_figure.add_plot(row=0, col=0)
w2 = bec_figure.add_plot(row=0, col=1)
w3 = bec_figure.add_plot(row=1, col=0)
w4 = bec_figure.add_plot(row=1, col=1)
bec_figure.change_layout(max_columns=1)
@@ -215,12 +207,12 @@ def test_change_layout(bec_figure):
def test_clear_all(bec_figure):
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
bec_figure.add_plot(widget_id="test_waveform_2", row=0, col=1)
bec_figure.add_plot(widget_id="test_waveform_3", row=1, col=0)
bec_figure.add_plot(widget_id="test_waveform_4", row=1, col=1)
bec_figure.add_plot(row=0, col=0)
bec_figure.add_plot(row=0, col=1)
bec_figure.add_plot(row=1, col=0)
bec_figure.add_plot(row=1, col=1)
bec_figure.clear_all()
assert len(bec_figure.widgets) == 0
assert len(bec_figure._widgets) == 0
assert np.shape(bec_figure.grid) == (0,)

View File

@@ -10,7 +10,7 @@ from .test_bec_figure import bec_figure
def test_adding_curve_to_waveform(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
# adding curve which is in bec - only names
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
@@ -38,7 +38,7 @@ def test_adding_curve_to_waveform(bec_figure):
def test_adding_curve_with_same_id(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i", gui_id="test_curve")
with pytest.raises(ValueError) as excinfo:
@@ -101,7 +101,7 @@ def test_create_waveform1D_by_config(bec_figure):
},
}
w1 = bec_figure.add_plot(widget_id="test_waveform", config=w1_config_input)
w1 = bec_figure.add_plot(config=w1_config_input)
w1_config_output = w1.get_config()
@@ -111,7 +111,7 @@ def test_create_waveform1D_by_config(bec_figure):
def test_change_gui_id(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
w1.change_gui_id("new_id")
@@ -120,12 +120,12 @@ def test_change_gui_id(bec_figure):
def test_getting_curve(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i", gui_id="test_curve")
c1_expected_config = CurveConfig(
widget_class="BECCurve",
gui_id="test_curve",
parent_id="test_waveform",
parent_id="widget_1",
label="bpm4i-bpm4i",
color="#cc4778",
symbol="o",
@@ -142,7 +142,7 @@ def test_getting_curve(bec_figure):
)
assert w1.curves[0].config == c1_expected_config
assert w1.curves_data["scan_segment"]["bpm4i-bpm4i"].config == c1_expected_config
assert w1._curves_data["scan_segment"]["bpm4i-bpm4i"].config == c1_expected_config
assert w1.get_curve(0).config == c1_expected_config
assert w1.get_curve("bpm4i-bpm4i").config == c1_expected_config
assert c1.get_config(False) == c1_expected_config
@@ -150,7 +150,7 @@ def test_getting_curve(bec_figure):
def test_getting_curve_errors(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i", gui_id="test_curve")
with pytest.raises(ValueError) as excinfo:
@@ -167,18 +167,18 @@ def test_getting_curve_errors(bec_figure):
def test_add_curve(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
assert len(w1.curves) == 1
assert w1.curves_data["scan_segment"] == {"bpm4i-bpm4i": c1}
assert w1._curves_data["scan_segment"] == {"bpm4i-bpm4i": c1}
assert c1.config.label == "bpm4i-bpm4i"
assert c1.config.source == "scan_segment"
def test_remove_curve(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
w1.add_curve_scan(x_name="samx", y_name="bpm4i")
w1.add_curve_scan(x_name="samx", y_name="bpm3a")
@@ -186,7 +186,7 @@ def test_remove_curve(bec_figure):
w1.remove_curve("bpm3a-bpm3a")
assert len(w1.plot_item.curves) == 0
assert w1.curves_data["scan_segment"] == {}
assert w1._curves_data["scan_segment"] == {}
with pytest.raises(ValueError) as excinfo:
w1.remove_curve(1.2)
@@ -196,7 +196,7 @@ def test_remove_curve(bec_figure):
def test_change_curve_appearance_methods(bec_figure, qtbot):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
@@ -223,7 +223,7 @@ def test_change_curve_appearance_methods(bec_figure, qtbot):
def test_change_curve_appearance_args(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
@@ -251,7 +251,7 @@ def test_change_curve_appearance_args(bec_figure):
def test_set_custom_curve_data(bec_figure, qtbot):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_custom(
x=[1, 2, 3],
@@ -287,7 +287,7 @@ def test_set_custom_curve_data(bec_figure, qtbot):
def test_get_all_data(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_custom(
x=[1, 2, 3],
@@ -322,7 +322,7 @@ def test_get_all_data(bec_figure):
def test_curve_add_by_config(bec_figure):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1_config_input = {
"widget_class": "BECCurve",
@@ -353,7 +353,7 @@ def test_curve_add_by_config(bec_figure):
def test_scan_update(bec_figure, qtbot):
w1 = bec_figure.add_plot(widget_id="test_waveform")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
@@ -387,7 +387,7 @@ def test_scan_update(bec_figure, qtbot):
def test_scan_history_with_val_access(bec_figure, qtbot):
w1 = bec_figure.add_plot(widget_id="test_waveform_history_val")
w1 = bec_figure.add_plot()
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")