mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 01:37:53 +02:00
Compare commits
70 Commits
v0.38.1
...
94-bec-fig
| Author | SHA1 | Date | |
|---|---|---|---|
| d996ff9284 | |||
| 8b43eba282 | |||
| 9c822ec480 | |||
|
|
44b451e66b | ||
| a2ed2ebe00 | |||
| 8127fc2960 | |||
|
|
6171790f66 | ||
|
|
ebb36f62dd | ||
|
|
644f1031f6 | ||
|
|
fd711b475f | ||
|
|
57132a4721 | ||
| f71dc5c5ab | |||
| 4630d78fc2 | |||
| da640e888d | |||
|
|
35cd4fd6f1 | ||
|
|
f06e652b82 | ||
|
|
5fc8047c8f | ||
|
|
0363fd5194 | ||
|
|
826a5e9874 | ||
|
|
f668eb8b9b | ||
|
|
5964778a64 | ||
|
|
8135f68230 | ||
|
|
24c77376b2 | ||
|
|
f364afcb42 | ||
|
|
4051902f09 | ||
|
|
a28b9c8981 | ||
|
|
9a5c86ea35 | ||
|
|
08534a4739 | ||
|
|
1db77b969b | ||
|
|
99dce077c4 | ||
|
|
402adc44e8 | ||
|
|
c6bdf0b6a5 | ||
|
|
1c2fb8b972 | ||
| a61bf36df5 | |||
|
|
d678a85957 | ||
|
|
684592ae37 | ||
|
|
f0ed243c91 | ||
|
|
cba3863e5a | ||
|
|
1d26b23221 | ||
|
|
b827e9eaa7 | ||
|
|
60d150a411 | ||
|
|
c781b1b4e4 | ||
|
|
565e475ace | ||
|
|
7c15d75011 | ||
|
|
b676877242 | ||
|
|
7768e594b5 | ||
|
|
9ef331c272 | ||
|
|
4a1792c209 | ||
|
|
91447a2d62 | ||
|
|
ed5bdd99e6 | ||
|
|
feca7a3dcd | ||
|
|
2d9020358d | ||
|
|
51259097fa | ||
|
|
8a4aeb8dfe | ||
|
|
4b0542a513 | ||
|
|
bf04a4e04a | ||
|
|
fa4ca935bb | ||
|
|
b52e22d81f | ||
|
|
2f96e10b9d | ||
|
|
031cb094e7 | ||
|
|
8afc5f0c0c | ||
|
|
17f14581d7 | ||
|
|
8361736679 | ||
|
|
0b9927fcf5 | ||
|
|
8139e271de | ||
|
|
6fe08e6b82 | ||
|
|
968da6f558 | ||
|
|
11ae0b1054 | ||
| 5ebfd2a3c2 | |||
| b36131eed5 |
76
CHANGELOG.md
76
CHANGELOG.md
@@ -2,6 +2,82 @@
|
||||
|
||||
<!--next-version-placeholder-->
|
||||
|
||||
## v0.41.1 (2024-02-26)
|
||||
|
||||
### Fix
|
||||
|
||||
* **bec_dispatcher:** Handle redis connection errors more gracefully ([`a2ed2eb`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a2ed2ebe00c623eb183b03f8182ffd672fbf9e1e))
|
||||
* **bec_dispatcher:** Adapt code to redis connector refactoring ([`8127fc2`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/8127fc2960bebd3e862dbe55ac9401af4a6dccb6))
|
||||
|
||||
## v0.41.0 (2024-02-26)
|
||||
|
||||
### Feature
|
||||
|
||||
* **widgets/waveform1d:** Data can be exported from rendered curve ([`5fc8047`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/5fc8047c8ff971cdc2807d02743eae56d288f4d7))
|
||||
* **widgets/figure:** Clear_all method for BECFigure ([`0363fd5`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0363fd5194320a7ea868ef883f8022ea464d0298))
|
||||
* **widgets/Waveform1D:** Waveform1D can be fully constructed by config ([`9a5c86e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/9a5c86ea35178b9cab270fc35e668dd22f3ec8da))
|
||||
* **widgets/figure.py:** Dark/light theme changer ([`08534a4`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/08534a4739ec8e85d82a00ab639411dd0198e9d8))
|
||||
* **utils/entry_validator:** Possibility to validate add_scan_curve with current BEC session ([`1db77b9`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1db77b969bcf9b38716ae3d38bf4695b2b8c1f37))
|
||||
* **cli:** Added cli interface, rebased ([`a61bf36`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a61bf36df5d54ad44f78479c2474c4e38e68ed26))
|
||||
* Curve can be modified after adding to the plot ([`684592a`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/684592ae37e9dd5328a96018c78ca242e10395b2))
|
||||
* Waveform1d.py curves can be removed by identifier by order(int) or by curve_id(str) ([`f0ed243`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/f0ed243c9197b7d1aab0d99a15e9ba175708ec90))
|
||||
* Waveform1d.py curves can be stylised; access scan history by index or scanID ([`cba3863`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/cba3863e5a9ac1187ea643be67db6cfc36b44ee2))
|
||||
* Start method for BECFigure, jupyter console .ui added to git ([`1d26b23`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1d26b2322147d9ea5a6a245e1648c00986f80881))
|
||||
* Added @user_access from bec_lib.utils ([`b827e9e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b827e9eaa77f8b64433bb7a54e40ab5ccd86f4b6))
|
||||
* Plot can be removed from BECFigure ([`60d150a`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/60d150a41193aa7659285cf3612965f1a3c57244))
|
||||
* Figure.py create widget factory ([`c781b1b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/c781b1b4e4121c4ec6fc8871a4cdf6f494913138))
|
||||
* Waveform1d.py draft ([`565e475`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/565e475ace72ccc103d71ea98af1dcaf04f37861))
|
||||
* Rpc decorator to add methods to USER_ACCESS ([`b676877`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b6768772424a3ad5ee7e271de19131f8065eef09))
|
||||
* BECFigure and BECPlotBase created ([`9ef331c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/9ef331c272b88f725de9b8497fdf906056c0738b))
|
||||
* BECConnector -> mixin class for all BEC Widget to hook them to BEC client ([`91447a2`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/91447a2d6234de1e8f2bac792e822bfda556abba))
|
||||
|
||||
### Fix
|
||||
|
||||
* **cli/client_utils:** "__rpc__" pop from msg_results ([`ebb36f6`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ebb36f62ddc1c5013435f9e7727648b977b6b732))
|
||||
* **tests:** BECDispatcher fixture putted back ([`644f103`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/644f1031f6ff27064111565b0882cb8b2544aa2f))
|
||||
* **cli/rpc:** Rpc client can return any type of object + config dict of the widgets ([`fd711b4`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/fd711b475f268fbdb59739da0a428f0355b25bac))
|
||||
* **cli/rpc:** Server access children widget.find_widget_by_id(gui_id) ([`57132a4`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/57132a472165c55bf99e1994d09f5fe3586c24da))
|
||||
* **cli:** Fixed property access, rebased ([`f71dc5c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/f71dc5c5abdd6b8b585cb9b502b11ef513d7813e))
|
||||
* **rpc_server:** Fixed gui_id lookup ([`4630d78`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/4630d78fc28109da7daf53e49dd3cdb9b8084941))
|
||||
* **cli:** Fixed rpc construction of nested widgets ([`da640e8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/da640e888d575b536fdd5d7adbf1df3eda802219))
|
||||
* **plots/waveform1d:** Pandas import clean up, export curves with none skipped ([`35cd4fd`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/35cd4fd6f176ba670fad5d9fec44b305094280d6))
|
||||
* **widgets/plots:** Added placeholder for cleanup method to BECPlotBase ([`24c7737`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/24c77376b232c3846a1d6be360ec46acc077b48d))
|
||||
* **widget/figure:** Add cleanup method to disconnect all slots before removing Waveform1D from layout ([`a28b9c8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a28b9c8981d1058e4dc4146463f16c53413e8db9))
|
||||
* **rpc:** Added annotations to pass py3.9 tests ([`c6bdf0b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/c6bdf0b6a5b12c054863b101a3944efc366686cb))
|
||||
* **rpc:** Connection to on_rpc_update done through bec_dispatcher ([`1c2fb8b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1c2fb8b972d4cb28cead11989461aea010c4571d))
|
||||
* After removing plot from BECFigure, the coordinates are correctly resigned ([`d678a85`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/d678a85957c13c1fda2b52692c0d3b9b7ff40834))
|
||||
* Removed DI references, fixed set when adding plot by fig ([`7c15d75`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/7c15d750117aec9e75111853074630a44dca87ae))
|
||||
|
||||
## v0.40.1 (2024-02-23)
|
||||
|
||||
### Fix
|
||||
|
||||
* **utils/bec_dispatcher:** _do_disconnect_slot will shutdown consumer of slots/signals which were already disconnected ([`feca7a3`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/feca7a3dcde6d0befa415db64fc8f9bbf0c06e52))
|
||||
|
||||
## v0.40.0 (2024-02-16)
|
||||
|
||||
### Feature
|
||||
|
||||
* **utils.colors:** Golden_angle_color utility can return colors as a list of QColor, RGB or HEC ([`5125909`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/51259097fa23ff861eac3f7c63624ea591bf1bd3))
|
||||
|
||||
## v0.39.0 (2024-02-12)
|
||||
|
||||
### Feature
|
||||
|
||||
* Added full app with all motor movement related widgets into motor_control_compilations.py ([`fa4ca93`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/fa4ca935bb39fdba4c6500ce9569d47400190e65))
|
||||
* MotorCoordinateTable mode_switch added for "Individual" and "Start/Stop" modes ([`2f96e10`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/2f96e10b9deb76eedd8f6b6e201ba3b0e526a6f0))
|
||||
* Motor_control.py MotorCoordinateTable added basic version to store coordinates and show them in motor_map.py ([`031cb09`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/031cb094e7f8a7be4a295bea99b7ca8e095db8d7))
|
||||
* Active motors from motor_map.py can be changed by slot without changing the whole config ([`17f1458`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/17f14581d7c4662a2f5814ea477dfae8ef6de555))
|
||||
* Control panels compilations ([`8361736`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/83617366796ce2926650e38a1a9cec296befd3c6))
|
||||
* Comboboxes of motor selection are changed to orange if the motors are not connected yet ([`0b9927f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0b9927fcf5f46410d05187b2e5a83f97a6ca9246))
|
||||
* Motor_control.py MotorControl widgets - Absolute + Relative movement, MotorSelection, ErrorMessage popups ([`6fe08e6`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/6fe08e6b8206bcaaa292b7ff0e6b0d32b883f24f))
|
||||
|
||||
## v0.38.2 (2024-02-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* Adapt code to BEC 1.0 ([`b36131e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b36131eed5c3a3ea58c0fa4d083e63a3717cdf22))
|
||||
|
||||
## v0.38.1 (2024-01-26)
|
||||
|
||||
### Fix
|
||||
|
||||
0
bec_widgets/cli/__init__.py
Normal file
0
bec_widgets/cli/__init__.py
Normal file
386
bec_widgets/cli/client.py
Normal file
386
bec_widgets/cli/client.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
|
||||
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECFigureClientMixin
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
|
||||
class BECPlotBase(RPCBase):
|
||||
@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 plot_data(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 BECWaveform1D(RPCBase):
|
||||
@rpc_call
|
||||
def add_curve_scan(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
x_entry: "Optional[str]" = None,
|
||||
y_entry: "Optional[str]" = None,
|
||||
color: "Optional[str]" = None,
|
||||
label: "Optional[str]" = None,
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Add a curve to the plot widget from the scan segment.
|
||||
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.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_curve_custom(
|
||||
self,
|
||||
x: "list | np.ndarray",
|
||||
y: "list | np.ndarray",
|
||||
label: "str" = None,
|
||||
color: "str" = None,
|
||||
**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.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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).
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def scan_history(self, scan_index: "int" = None, scanID: "str" = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scanID or scan_index.
|
||||
Args:
|
||||
scanID(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.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def curves(self) -> "list[BECCurve]":
|
||||
"""
|
||||
Get the curves of the plot widget as a list
|
||||
Returns:
|
||||
list: List of curves.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def curves_data(self) -> "dict":
|
||||
"""
|
||||
Get the curves data of the plot widget as a dictionary
|
||||
Returns:
|
||||
dict: Dictionary of curves data.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def apply_config(self, config: "dict | WidgetConfig", replot_last_scan: "bool" = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
Args:
|
||||
config(dict|WidgetConfig): Configuration settings.
|
||||
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class BECFigure(RPCBase, BECFigureClientMixin):
|
||||
@rpc_call
|
||||
def add_plot(
|
||||
self,
|
||||
widget_id: "str" = None,
|
||||
row: "int" = None,
|
||||
col: "int" = None,
|
||||
config=None,
|
||||
**axis_kwargs
|
||||
) -> "BECWaveform1D":
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def clear_all(self):
|
||||
"""
|
||||
Clear all widgets from the figure and reset to default state
|
||||
"""
|
||||
|
||||
|
||||
class BECCurve(RPCBase):
|
||||
@rpc_call
|
||||
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"]
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_data(self, x, y):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol(self, symbol: "str"):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_color(self, symbol_color: "str"):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_size(self, symbol_size: "int"):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_pen_width(self, pen_width: "int"):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
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.
|
||||
"""
|
||||
175
bec_widgets/cli/client_utils.py
Normal file
175
bec_widgets/cli/client_utils.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import importlib
|
||||
import select
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
from functools import wraps
|
||||
|
||||
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
|
||||
|
||||
|
||||
def rpc_call(func):
|
||||
"""
|
||||
A decorator for calling a function on the server.
|
||||
|
||||
Args:
|
||||
func: The function to call.
|
||||
|
||||
Returns:
|
||||
The result of the function call.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return self._run_rpc(func.__name__, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class BECFigureClientMixin:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._process = None
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
Show the figure.
|
||||
"""
|
||||
if self._process is None or self._process.poll() is not None:
|
||||
self._start_plot_process()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the figure.
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
self._run_rpc("close", (), wait_for_rpc_response=False)
|
||||
self._process.kill()
|
||||
self._process = None
|
||||
|
||||
def _start_plot_process(self) -> None:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
"""
|
||||
# pylint: disable=subprocess-run-check
|
||||
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
||||
monitor_path = monitor_module.__file__
|
||||
|
||||
command = f"python {monitor_path} --id {self._gui_id}"
|
||||
self._process = subprocess.Popen(
|
||||
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
def print_log(self) -> None:
|
||||
"""
|
||||
Print the log of the plot process.
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
print(self._get_stderr_output())
|
||||
|
||||
def _get_stderr_output(self) -> str:
|
||||
stderr_output = []
|
||||
while self._process.poll() is not None:
|
||||
readylist, _, _ = select.select([self._process.stderr], [], [], 0.1)
|
||||
if not readylist:
|
||||
break
|
||||
line = self._process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
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:
|
||||
self._client = BECDispatcher().client
|
||||
self._config = config if config is not None else {}
|
||||
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}")
|
||||
|
||||
@property
|
||||
def _root(self):
|
||||
"""
|
||||
Get the root widget. This is the BECFigure widget that holds
|
||||
the anchor gui_id.
|
||||
"""
|
||||
parent = self
|
||||
# pylint: disable=protected-access
|
||||
while parent._parent is not None:
|
||||
parent = parent._parent
|
||||
return parent
|
||||
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, **kwargs):
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
Args:
|
||||
method: The method to call.
|
||||
args: The arguments to pass to the method.
|
||||
wait_for_rpc_response: Whether to wait for the RPC response.
|
||||
kwargs: The keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
The result of the RPC call.
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
rpc_msg = messages.GUIInstructionMessage(
|
||||
action=method,
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
|
||||
metadata={"request_id": request_id},
|
||||
)
|
||||
print(f"RPCBase: {rpc_msg}")
|
||||
# pylint: disable=protected-access
|
||||
receiver = self._root._gui_id
|
||||
self._client.connector.send(MessageEndpoints.gui_instructions(receiver), rpc_msg)
|
||||
|
||||
if not wait_for_rpc_response:
|
||||
return None
|
||||
response = self._wait_for_response(request_id)
|
||||
# get class name
|
||||
if not response.content["accepted"]:
|
||||
raise ValueError(response.content["message"]["error"])
|
||||
msg_result = response.content["message"].get("result")
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
|
||||
def _create_widget_from_msg_result(self, msg_result):
|
||||
if msg_result is None:
|
||||
return None
|
||||
if isinstance(msg_result, list):
|
||||
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
|
||||
cls = msg_result.pop("widget_class", None)
|
||||
msg_result.pop("__rpc__", None)
|
||||
|
||||
if not cls:
|
||||
return msg_result
|
||||
|
||||
cls = getattr(client, cls)
|
||||
print(msg_result)
|
||||
return cls(parent=self, **msg_result)
|
||||
return msg_result
|
||||
|
||||
def _wait_for_response(self, request_id):
|
||||
"""
|
||||
Wait for the response from the server.
|
||||
"""
|
||||
response = None
|
||||
while response is None:
|
||||
response = self._client.connector.get(
|
||||
MessageEndpoints.gui_instruction_response(request_id)
|
||||
)
|
||||
QCoreApplication.processEvents() # keep UI responsive (and execute signals/slots)
|
||||
return response
|
||||
96
bec_widgets/cli/generate_cli.py
Normal file
96
bec_widgets/cli/generate_cli.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECFigureClientMixin
|
||||
from typing import Literal, Optional, overload"""
|
||||
|
||||
self.content = ""
|
||||
|
||||
def generate_client(self, published_classes: list):
|
||||
"""
|
||||
Generate the client for the published classes.
|
||||
|
||||
Args:
|
||||
published_classes(list): The list of published classes (e.g. [BECWaveform1D, BECFigure]).
|
||||
"""
|
||||
for cls in published_classes:
|
||||
self.content += "\n\n"
|
||||
self.generate_content_for_class(cls)
|
||||
|
||||
def generate_content_for_class(self, cls):
|
||||
"""
|
||||
Generate the content for the class.
|
||||
Args:
|
||||
cls: The class for which to generate the content.
|
||||
"""
|
||||
|
||||
class_name = cls.__name__
|
||||
module = cls.__module__
|
||||
|
||||
# Generate the header
|
||||
# self.header += f"""
|
||||
# from {module} import {class_name}"""
|
||||
|
||||
# Generate the content
|
||||
if cls.__name__ == "BECFigure":
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase, BECFigureClientMixin):"""
|
||||
else:
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase):"""
|
||||
for method in cls.USER_ACCESS:
|
||||
obj = getattr(cls, method)
|
||||
if isinstance(obj, property):
|
||||
self.content += """
|
||||
@property
|
||||
@rpc_call"""
|
||||
sig = str(inspect.signature(obj.fget))
|
||||
doc = inspect.getdoc(obj.fget)
|
||||
else:
|
||||
sig = str(inspect.signature(obj))
|
||||
doc = inspect.getdoc(obj)
|
||||
overloads = typing.get_overloads(obj)
|
||||
for overload in overloads:
|
||||
sig_overload = str(inspect.signature(overload))
|
||||
self.content += f"""
|
||||
@overload
|
||||
def {method}{str(sig_overload)}: ...
|
||||
"""
|
||||
|
||||
self.content += """
|
||||
@rpc_call"""
|
||||
self.content += f"""
|
||||
def {method}{str(sig)}:
|
||||
\"\"\"
|
||||
{doc}
|
||||
\"\"\""""
|
||||
|
||||
def write(self, file_name: str):
|
||||
"""
|
||||
Write the content to a file.
|
||||
|
||||
Args:
|
||||
file_name(str): The name of the file to write to.
|
||||
"""
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(self.header)
|
||||
file.write(self.content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.plots import BECPlotBase, BECWaveform1D # ,BECCurve
|
||||
from bec_widgets.widgets.plots.waveform1d import BECCurve
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
clss = [BECPlotBase, BECWaveform1D, BECFigure, BECCurve]
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client(clss)
|
||||
generator.write(client_path)
|
||||
110
bec_widgets/cli/server.py
Normal file
110
bec_widgets/cli/server.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import inspect
|
||||
|
||||
from bec_lib import MessageEndpoints, messages
|
||||
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.plots import BECCurve, BECWaveform1D
|
||||
|
||||
|
||||
class BECWidgetsCLIServer:
|
||||
WIDGETS = [BECWaveform1D, BECFigure, BECCurve]
|
||||
|
||||
def __init__(self, gui_id: str = None, dispatcher: BECDispatcher = None) -> None:
|
||||
self.dispatcher = BECDispatcher() if dispatcher is None else dispatcher
|
||||
self.client = self.dispatcher.client
|
||||
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}")
|
||||
|
||||
self.dispatcher.connect_slot(
|
||||
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Start the figure window."""
|
||||
self.fig.start()
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
try:
|
||||
method = msg["action"]
|
||||
args = msg["parameter"].get("args", [])
|
||||
kwargs = msg["parameter"].get("kwargs", {})
|
||||
obj = self.get_object_from_config(msg["parameter"])
|
||||
res = self.run_rpc(obj, method, args, kwargs)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.send_response(request_id, False, {"error": str(e)})
|
||||
else:
|
||||
self.send_response(request_id, True, {"result": res})
|
||||
|
||||
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_instruction_response(request_id),
|
||||
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
||||
expire=60,
|
||||
)
|
||||
|
||||
def get_object_from_config(self, config: dict):
|
||||
gui_id = config.get("gui_id")
|
||||
# check if the object is the figure
|
||||
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"]]
|
||||
return obj
|
||||
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")
|
||||
|
||||
def run_rpc(self, obj, method, args, kwargs):
|
||||
method_obj = getattr(obj, method)
|
||||
# check if the method accepts args and kwargs
|
||||
if not callable(method_obj):
|
||||
res = method_obj
|
||||
else:
|
||||
sig = inspect.signature(method_obj)
|
||||
if sig.parameters:
|
||||
res = method_obj(*args, **kwargs)
|
||||
else:
|
||||
res = method_obj()
|
||||
|
||||
if isinstance(res, list):
|
||||
res = [self.serialize_object(obj) for obj in res]
|
||||
else:
|
||||
res = self.serialize_object(res)
|
||||
return res
|
||||
|
||||
def serialize_object(self, obj):
|
||||
if isinstance(obj, BECConnector):
|
||||
return {
|
||||
"gui_id": obj.gui_id,
|
||||
"widget_class": obj.__class__.__name__,
|
||||
"config": obj.config.model_dump(),
|
||||
"__rpc__": True,
|
||||
}
|
||||
return obj
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
||||
parser.add_argument("--id", type=str, help="The id of the server")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
server = BECWidgetsCLIServer(gui_id=args.id)
|
||||
# server = BECWidgetsCLIServer(gui_id="test")
|
||||
server.start()
|
||||
@@ -0,0 +1,9 @@
|
||||
from .motor_movement import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelRelative,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ class StreamApp(QWidget):
|
||||
|
||||
@staticmethod
|
||||
def _streamer_cb(msg, *, parent, **_kwargs) -> None:
|
||||
msgMCS = messages.DeviceMessage.loads(msg.value)
|
||||
msgMCS = msg.value
|
||||
print(msgMCS)
|
||||
row = msgMCS.content["signals"][parent.sub_device]
|
||||
metadata = msgMCS.metadata
|
||||
@@ -123,7 +123,7 @@ class StreamApp(QWidget):
|
||||
def _device_cv(msg, *, parent, **_kwargs) -> None:
|
||||
print("Getting ScanID")
|
||||
|
||||
msgDEV = messages.ScanStatusMessage.loads(msg.value)
|
||||
msgDEV = msg.value
|
||||
|
||||
current_scanID = msgDEV.content["scanID"]
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from bec_lib import messages, MessageEndpoints, RedisConnector
|
||||
import time
|
||||
|
||||
connector = RedisConnector("localhost:6379")
|
||||
producer = connector.producer()
|
||||
metadata = {}
|
||||
|
||||
scanID = "ScanID1"
|
||||
@@ -20,13 +19,11 @@ for ii in range(20):
|
||||
metadata=metadata,
|
||||
).dumps()
|
||||
|
||||
# producer.send(topic=MessageEndpoints.device_status(device="mca"), msg=msg)
|
||||
|
||||
producer.xadd(
|
||||
connector.xadd(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scanID=scanID, device="mca"
|
||||
), # scanID will be different for each scan
|
||||
msg={"data": msg},
|
||||
msg={"data": msg}, # TODO should be msg_dict
|
||||
expire=1800,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from .motor_control_compilations import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelRelative,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QVBoxLayout
|
||||
from qtpy.QtWidgets import (
|
||||
QWidget,
|
||||
QSplitter,
|
||||
)
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.widgets import (
|
||||
MotorControlAbsolute,
|
||||
MotorControlRelative,
|
||||
MotorControlSelection,
|
||||
MotorThread,
|
||||
MotorMap,
|
||||
MotorCoordinateTable,
|
||||
)
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"motor_control": {
|
||||
"motor_x": "samx",
|
||||
"motor_y": "samy",
|
||||
"step_size_x": 3,
|
||||
"step_size_y": 3,
|
||||
"precision": 4,
|
||||
"step_x_y_same": False,
|
||||
"move_with_arrows": False,
|
||||
},
|
||||
"plot_settings": {
|
||||
"colormap": "Greys",
|
||||
"scatter_size": 5,
|
||||
"max_points": 1000,
|
||||
"num_dim_points": 100,
|
||||
"precision": 2,
|
||||
"num_columns": 1,
|
||||
"background_value": 25,
|
||||
},
|
||||
"motors": [
|
||||
{
|
||||
"plot_name": "Motor Map",
|
||||
"x_label": "Motor X",
|
||||
"y_label": "Motor Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "samy", "entry": "samy"}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class MotorControlApp(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
# Widgets
|
||||
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
|
||||
# Create MotorMap
|
||||
self.motion_map = MotorMap(client=self.client, config=self.config)
|
||||
# Create MotorCoordinateTable
|
||||
self.motor_table = MotorCoordinateTable(client=self.client, config=self.config)
|
||||
|
||||
# Create the splitter and add MotorMap and MotorControlPanel
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
splitter.addWidget(self.motion_map)
|
||||
splitter.addWidget(self.motor_control_panel)
|
||||
splitter.addWidget(self.motor_table)
|
||||
|
||||
# Set the main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(splitter)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.motor_control_panel.selection_widget.selected_motors_signal.connect(
|
||||
lambda x, y: self.motion_map.change_motors(x, y, 0)
|
||||
)
|
||||
self.motor_control_panel.absolute_widget.coordinates_signal.connect(
|
||||
self.motor_table.add_coordinate
|
||||
)
|
||||
self.motor_control_panel.relative_widget.precision_signal.connect(
|
||||
self.motor_table.set_precision
|
||||
)
|
||||
self.motor_control_panel.relative_widget.precision_signal.connect(
|
||||
self.motor_control_panel.absolute_widget.set_precision
|
||||
)
|
||||
|
||||
self.motor_table.plot_coordinates_signal.connect(self.motion_map.plot_saved_coordinates)
|
||||
|
||||
|
||||
class MotorControlMap(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
# Widgets
|
||||
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
|
||||
# Create MotorMap
|
||||
self.motion_map = MotorMap(client=self.client, config=self.config)
|
||||
|
||||
# Create the splitter and add MotorMap and MotorControlPanel
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
splitter.addWidget(self.motion_map)
|
||||
splitter.addWidget(self.motor_control_panel)
|
||||
|
||||
# Set the main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(splitter)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.motor_control_panel.selection_widget.selected_motors_signal.connect(
|
||||
lambda x, y: self.motion_map.change_motors(x, y, 0)
|
||||
)
|
||||
|
||||
|
||||
class MotorControlPanel(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.relative_widget = MotorControlRelative(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.absolute_widget = MotorControlAbsolute(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.relative_widget)
|
||||
layout.addWidget(self.absolute_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
|
||||
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
|
||||
|
||||
# Set the window to a fixed size based on its contents
|
||||
self.layout().setSizeConstraint(layout.SetFixedSize)
|
||||
|
||||
|
||||
class MotorControlPanelAbsolute(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.absolute_widget = MotorControlAbsolute(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.absolute_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
|
||||
|
||||
# Set the window to a fixed size based on its contents
|
||||
self.layout().setSizeConstraint(layout.SetFixedSize)
|
||||
|
||||
|
||||
class MotorControlPanelRelative(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.relative_widget = MotorControlRelative(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.relative_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
|
||||
|
||||
# Set the window to a fixed size based on its contents
|
||||
self.layout().setSizeConstraint(layout.SetFixedSize)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description="Run various Motor Control Widgets compositions.")
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--variant",
|
||||
type=str,
|
||||
choices=["app", "map", "panel", "panel_abs", "panel_rel"],
|
||||
help="Select the variant of the motor control to run. "
|
||||
"'app' for the full application, "
|
||||
"'map' for MotorMap, "
|
||||
"'panel' for the MotorControlPanel, "
|
||||
"'panel_abs' for MotorControlPanel with absolute control, "
|
||||
"'panel_rel' for MotorControlPanel with relative control.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication([])
|
||||
qdarktheme.setup_theme("auto")
|
||||
|
||||
if args.variant == "app":
|
||||
window = MotorControlApp(client=client, config=CONFIG_DEFAULT)
|
||||
elif args.variant == "map":
|
||||
window = MotorControlMap(client=client, config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel":
|
||||
window = MotorControlPanel(client=client, config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel_abs":
|
||||
window = MotorControlPanelAbsolute(client=client, config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel_rel":
|
||||
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.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1297,7 +1297,7 @@ class MotorControl(QThread):
|
||||
|
||||
@staticmethod
|
||||
def _device_status_callback_motors(msg, *, parent, **_kwargs) -> None:
|
||||
deviceMSG = messages.DeviceMessage.loads(msg.value)
|
||||
deviceMSG = msg.value
|
||||
if parent.motor_x.name in deviceMSG.content["signals"]:
|
||||
parent.current_x = deviceMSG.content["signals"][parent.motor_x.name]["value"]
|
||||
elif parent.motor_y.name in deviceMSG.content["signals"]:
|
||||
|
||||
@@ -40,7 +40,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
uic.loadUi(os.path.join(current_path, "line_plot.ui"), self)
|
||||
|
||||
self._idle_time = 100
|
||||
self.producer = RedisConnector(["localhost:6379"]).producer()
|
||||
self.connector = RedisConnector(["localhost:6379"])
|
||||
|
||||
self.y_value_list = y_value_list
|
||||
self.previous_y_value_list = None
|
||||
@@ -214,7 +214,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
]
|
||||
}
|
||||
msg = messages.DeviceMessage(signals=return_dict).dumps()
|
||||
self.producer.set_and_publish("px_stream/gui_event", msg=msg)
|
||||
self.connector.set_and_publish("px_stream/gui_event", msg=msg)
|
||||
self.roi_signal.emit(region)
|
||||
|
||||
def init_table(self):
|
||||
@@ -270,8 +270,8 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
endpoint = f"px_stream/projection_{self._current_proj}/data"
|
||||
msgs = self.client.producer.lrange(topic=endpoint, start=-1, end=-1)
|
||||
data = [messages.DeviceMessage.loads(msg) for msg in msgs]
|
||||
msgs = self.client.connector.lrange(topic=endpoint, start=-1, end=-1)
|
||||
data = msgs
|
||||
if not data:
|
||||
continue
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
@@ -295,7 +295,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
def new_proj(self, content: dict, _metadata: dict):
|
||||
proj_nr = content["signals"]["proj_nr"]
|
||||
endpoint = f"px_stream/projection_{proj_nr}/metadata"
|
||||
msg_raw = self.client.producer.get(topic=endpoint)
|
||||
msg_raw = self.client.connector.get(topic=endpoint)
|
||||
msg = messages.DeviceMessage.loads(msg_raw)
|
||||
self._current_q = msg.content["signals"]["q"]
|
||||
self._current_norm = msg.content["signals"]["norm_sum"]
|
||||
|
||||
@@ -2,3 +2,7 @@ from .crosshair import Crosshair
|
||||
from .colors import Colors
|
||||
from .validator_delegate import DoubleValidationDelegate
|
||||
from .bec_table import BECTable
|
||||
from .bec_connector import BECConnector, ConnectionConfig
|
||||
from .bec_dispatcher import BECDispatcher
|
||||
from .rpc_decorator import rpc_public, register_rpc_methods
|
||||
from .entry_validator import EntryValidator
|
||||
|
||||
108
bec_widgets/utils/bec_connector.py
Normal file
108
bec_widgets/utils/bec_connector.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Type, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
|
||||
class ConnectionConfig(BaseModel):
|
||||
"""Configuration for BECConnector mixin class"""
|
||||
|
||||
widget_class: str = Field(default="NonSpecifiedWidget", description="The class of the widget.")
|
||||
gui_id: Optional[str] = Field(
|
||||
default=None, validate_default=True, description="The GUI ID of the widget."
|
||||
)
|
||||
|
||||
@field_validator("gui_id")
|
||||
def generate_gui_id(cls, v, values):
|
||||
"""Generate a GUI ID if none is provided."""
|
||||
if v is None:
|
||||
widget_class = values.data["widget_class"]
|
||||
v = f"{widget_class}_{str(time.time())}"
|
||||
return v
|
||||
return v
|
||||
|
||||
|
||||
class BECConnector:
|
||||
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher()
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
|
||||
if config:
|
||||
self.config = config
|
||||
else:
|
||||
print(
|
||||
f"No initial config found for {self.__class__.__name__}.\n"
|
||||
f"Initializing with default config."
|
||||
)
|
||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
|
||||
if gui_id:
|
||||
self.config.gui_id = gui_id
|
||||
self.gui_id = gui_id
|
||||
else:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
Set the GUI ID for the widget.
|
||||
Args:
|
||||
gui_id(str): GUI ID
|
||||
"""
|
||||
self.config.gui_id = gui_id
|
||||
self.gui_id = gui_id
|
||||
|
||||
def get_obj_by_id(self, obj_id: str):
|
||||
if obj_id == self.gui_id:
|
||||
return self
|
||||
|
||||
def get_bec_shortcuts(self):
|
||||
"""Get BEC shortcuts for the widget."""
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
self.queue = self.client.queue
|
||||
self.scan_storage = self.queue.scan_storage
|
||||
self.dap = self.client.dap
|
||||
|
||||
def update_client(self, client) -> None:
|
||||
"""Update the client and device manager from BEC and create object for BEC shortcuts.
|
||||
Args:
|
||||
client: BEC client
|
||||
"""
|
||||
self.client = client
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@pyqtSlot(ConnectionConfig) # TODO can be also dict
|
||||
def on_config_update(self, config: ConnectionConfig | dict) -> None:
|
||||
"""
|
||||
Update the configuration for the widget.
|
||||
Args:
|
||||
config(ConnectionConfig): Configuration settings.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = ConnectionConfig(**config)
|
||||
# TODO add error handler
|
||||
|
||||
self.config = config
|
||||
|
||||
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.
|
||||
"""
|
||||
if dict_output:
|
||||
return self.config.model_dump()
|
||||
else:
|
||||
return self.config
|
||||
@@ -1,7 +1,3 @@
|
||||
# TODO last backup
|
||||
|
||||
# todo super last refactor
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -10,9 +6,10 @@ import os
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
|
||||
from bec_lib import BECClient, messages, ServiceConfig
|
||||
from bec_lib.redis_connector import RedisConsumerThreaded
|
||||
from qtpy.QtCore import QObject, Signal as pyqtSignal
|
||||
import redis
|
||||
from bec_lib import BECClient, ServiceConfig
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
# Adding a new pyqt signal requires a class factory, as they must be part of the class definition
|
||||
# and cannot be dynamically added as class attributes after the class has been defined.
|
||||
@@ -22,10 +19,11 @@ _signal_class_factory = (
|
||||
|
||||
|
||||
class _Connection:
|
||||
"""Utility class to keep track of slots connected to a particular redis consumer"""
|
||||
"""Utility class to keep track of slots connected to a particular redis connector"""
|
||||
|
||||
def __init__(self, callback) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def __init__(self, consumer) -> None:
|
||||
self.consumer: RedisConsumerThreaded = consumer
|
||||
self.slots = set()
|
||||
# keep a reference to a new signal class, so it is not gc'ed
|
||||
self._signal_container = next(_signal_class_factory)()
|
||||
@@ -33,18 +31,21 @@ class _Connection:
|
||||
|
||||
|
||||
class _BECDispatcher(QObject):
|
||||
"""Utility class to keep track of slots connected to a particular redis consumer"""
|
||||
"""Utility class to keep track of slots connected to a particular redis connector"""
|
||||
|
||||
def __init__(self, bec_config=None):
|
||||
super().__init__()
|
||||
self.client = BECClient()
|
||||
self.client = BECClient(forced=True) # make a new instance
|
||||
|
||||
# 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.initialize(config=ServiceConfig(config_path=bec_config))
|
||||
try:
|
||||
self.client.initialize(config=ServiceConfig(config_path=bec_config))
|
||||
except redis.exceptions.ConnectionError as e:
|
||||
print(f"Failed to initialize BECClient: {e}")
|
||||
self._connections = {}
|
||||
|
||||
def connect_slot(
|
||||
@@ -76,28 +77,28 @@ class _BECDispatcher(QObject):
|
||||
"""Creates a new connection for given topics."""
|
||||
|
||||
def cb(msg):
|
||||
msg = messages.MessageReader.loads(msg.value)
|
||||
if not isinstance(msg, list):
|
||||
msg = [msg]
|
||||
for msg_i in msg:
|
||||
for connection_key, connection in self._connections.items():
|
||||
if set(topics).intersection(
|
||||
connection_key if isinstance(connection_key, tuple) else [connection_key]
|
||||
):
|
||||
connection.signal.emit(msg_i.content, msg_i.metadata)
|
||||
msg = msg.value
|
||||
for connection_key, connection in self._connections.items():
|
||||
if set(topics).intersection(connection_key):
|
||||
connection.signal.emit(msg.content, msg.metadata)
|
||||
|
||||
consumer = self.client.connector.consumer(topics=topics, cb=cb)
|
||||
consumer.start()
|
||||
return _Connection(consumer)
|
||||
try:
|
||||
self.client.connector.register(topics=topics, cb=cb)
|
||||
except redis.exceptions.ConnectionError:
|
||||
print("Could not connect to Redis, skipping registration of topics.")
|
||||
|
||||
return _Connection(cb)
|
||||
|
||||
def _do_disconnect_slot(self, topic, slot):
|
||||
print(f"Disconnecting {slot} from {topic}")
|
||||
connection = self._connections[topic]
|
||||
connection.signal.disconnect(slot)
|
||||
try:
|
||||
connection.signal.disconnect(slot)
|
||||
except TypeError:
|
||||
print(f"Could not disconnect slot:'{slot}' from topic:'{topic}'")
|
||||
print("Continue to remove slot:'{slot}' from 'connection.slots'.")
|
||||
connection.slots.remove(slot)
|
||||
if not connection.slots:
|
||||
print(f"{connection.consumer} is shutting down")
|
||||
connection.consumer.shutdown()
|
||||
connection.consumer.join()
|
||||
del self._connections[topic]
|
||||
|
||||
def _disconnect_slot_from_topic(self, slot: Callable, topic: str) -> None:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph import mkColor
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
|
||||
class Colors:
|
||||
@@ -10,6 +12,9 @@ class Colors:
|
||||
|
||||
Args:
|
||||
num (int): Number of angles
|
||||
|
||||
Returns:
|
||||
list: List of angles calculated using the golden ratio.
|
||||
"""
|
||||
phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2)
|
||||
angles = []
|
||||
@@ -21,30 +26,40 @@ class Colors:
|
||||
return angles
|
||||
|
||||
@staticmethod
|
||||
def golden_angle_color(colormap: str, num: int) -> list:
|
||||
def golden_angle_color(
|
||||
colormap: str, num: int, format: Literal["QColor", "HEX", "RGB"] = "QColor"
|
||||
) -> list:
|
||||
"""
|
||||
Extract num colors for from the specified colormap following golden angle distribution.
|
||||
Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
|
||||
|
||||
Args:
|
||||
colormap (str): Name of the colormap
|
||||
num (int): Number of requested colors
|
||||
colormap (str): Name of the colormap.
|
||||
num (int): Number of requested colors.
|
||||
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
|
||||
|
||||
Returns:
|
||||
list: List of colors with length <num>
|
||||
list: List of colors in the specified format.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of requested colors is greater than the number of colors in the colormap.
|
||||
"""
|
||||
|
||||
cmap = pg.colormap.get(colormap)
|
||||
cmap_colors = cmap.color
|
||||
cmap_colors = cmap.getColors(mode="float")
|
||||
if num > len(cmap_colors):
|
||||
raise ValueError(
|
||||
f"Number of colors requested ({num}) is greater than the number of colors in the colormap ({len(cmap_colors)})"
|
||||
)
|
||||
angles = Colors.golden_ratio(len(cmap_colors))
|
||||
color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
|
||||
colors = [
|
||||
mkColor(tuple((cmap_colors[int(ii)] * 255).astype(int))) for ii in color_selection[:num]
|
||||
]
|
||||
colors = []
|
||||
for ii in color_selection[:num]:
|
||||
color = cmap_colors[int(ii)]
|
||||
if format.upper() == "HEX":
|
||||
colors.append(QColor.fromRgbF(*color).name())
|
||||
elif format.upper() == "RGB":
|
||||
colors.append(tuple((np.array(color) * 255).astype(int)))
|
||||
elif format.upper() == "QCOLOR":
|
||||
colors.append(QColor.fromRgbF(*color))
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
return colors
|
||||
|
||||
17
bec_widgets/utils/entry_validator.py
Normal file
17
bec_widgets/utils/entry_validator.py
Normal file
@@ -0,0 +1,17 @@
|
||||
class EntryValidator:
|
||||
def __init__(self, devices):
|
||||
self.devices = devices
|
||||
|
||||
def validate_signal(self, name: str, entry: str = None) -> str:
|
||||
if name not in self.devices:
|
||||
raise ValueError(f"Device '{name}' not found in current BEC session")
|
||||
|
||||
device = self.devices[name]
|
||||
description = device.describe()
|
||||
|
||||
if entry is None:
|
||||
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
|
||||
if entry not in description:
|
||||
raise ValueError(f"Entry '{entry}' not found in device '{name}' signals")
|
||||
|
||||
return entry
|
||||
15
bec_widgets/utils/rpc_decorator.py
Normal file
15
bec_widgets/utils/rpc_decorator.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def rpc_public(func):
|
||||
func.rpc_public = True # Mark the function for later processing by the class decorator
|
||||
return func
|
||||
|
||||
|
||||
def register_rpc_methods(cls):
|
||||
"""
|
||||
Class decorator to scan for rpc_public methods and add them to USER_ACCESS.
|
||||
"""
|
||||
if not hasattr(cls, "USER_ACCESS"):
|
||||
cls.USER_ACCESS = set()
|
||||
for name, method in cls.__dict__.items():
|
||||
if getattr(method, "rpc_public", False):
|
||||
cls.USER_ACCESS.add(name)
|
||||
return cls
|
||||
@@ -4,3 +4,12 @@ from .scan_control import ScanControl
|
||||
from .toolbar import ModularToolBar
|
||||
from .editor import BECEditor
|
||||
from .monitor_scatter_2D import BECMonitor2DScatter
|
||||
from .motor_control import (
|
||||
MotorControlRelative,
|
||||
MotorControlAbsolute,
|
||||
MotorControlSelection,
|
||||
MotorThread,
|
||||
MotorCoordinateTable,
|
||||
)
|
||||
from .figure import FigureConfig, BECFigure
|
||||
from .plots import BECWaveform1D, BECCurve, BECPlotBase
|
||||
|
||||
1
bec_widgets/widgets/figure/__init__.py
Normal file
1
bec_widgets/widgets/figure/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .figure import FigureConfig, BECFigure
|
||||
524
bec_widgets/widgets/figure/figure.py
Normal file
524
bec_widgets/widgets/figure/figure.py
Normal file
@@ -0,0 +1,524 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import qdarktheme
|
||||
from pydantic import Field
|
||||
from pyqtgraph.Qt import uic
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
from qtpy.QtWidgets import QVBoxLayout, QMainWindow
|
||||
|
||||
from bec_widgets.utils import BECConnector, BECDispatcher, ConnectionConfig
|
||||
from bec_widgets.widgets.plots import BECPlotBase, BECWaveform1D, Waveform1DConfig, WidgetConfig
|
||||
|
||||
|
||||
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, WidgetConfig] = Field(
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
|
||||
class WidgetHandler:
|
||||
"""Factory for creating and configuring BEC widgets for BECFigure."""
|
||||
|
||||
def __init__(self):
|
||||
self.widget_factory = {
|
||||
"PlotBase": (BECPlotBase, WidgetConfig),
|
||||
"Waveform1D": (BECWaveform1D, Waveform1DConfig),
|
||||
}
|
||||
|
||||
def create_widget(
|
||||
self,
|
||||
widget_type: str,
|
||||
widget_id: 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
|
||||
widget_config_dict = {
|
||||
"widget_class": widget_class.__name__,
|
||||
"parent_id": parent_id,
|
||||
"gui_id": widget_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(BECConnector, pg.GraphicsLayoutWidget):
|
||||
USER_ACCESS = ["add_plot", "remove", "change_layout", "change_theme", "clear_all"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
config: Optional[FigureConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = FigureConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = FigureConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
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 = []
|
||||
|
||||
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.
|
||||
"""
|
||||
qdarktheme.setup_theme(theme)
|
||||
self.setBackground("k" if theme == "dark" else "w")
|
||||
self.config.theme = theme
|
||||
|
||||
def add_plot(
|
||||
self, widget_id: str = None, row: int = None, col: int = None, config=None, **axis_kwargs
|
||||
) -> BECWaveform1D:
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
return self.add_widget(
|
||||
widget_type="Waveform1D",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget_type: Literal["PlotBase", "Waveform1D"] = "PlotBase",
|
||||
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 = self._generate_unique_widget_id()
|
||||
if widget_id in self.widgets:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
|
||||
|
||||
widget = self.widget_handler.create_widget(
|
||||
widget_type=widget_type,
|
||||
widget_id=widget_id,
|
||||
parent_figure=self,
|
||||
parent_id=self.gui_id,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
# 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:
|
||||
widget.config.row = row
|
||||
widget.config.col = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.addItem(widget, row=row, col=col)
|
||||
else:
|
||||
row, col = self._find_next_empty_position()
|
||||
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 _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._get_widget_by_coordinates(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()
|
||||
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)
|
||||
print(f"Removed widget {widget_id}.")
|
||||
else:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
|
||||
|
||||
def __getitem__(self, key: tuple | str):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
return self._get_widget_by_coordinates(*key)
|
||||
elif 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 _get_widget_by_coordinates(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 _generate_unique_widget_id(self):
|
||||
"""Generate a unique widget ID."""
|
||||
existing_ids = set(self.widgets.keys())
|
||||
for i in itertools.count(1):
|
||||
widget_id = f"widget_{i}"
|
||||
if widget_id not in existing_ids:
|
||||
return widget_id
|
||||
|
||||
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."""
|
||||
print(f"old grid: {self.grid}")
|
||||
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, 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
|
||||
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"""
|
||||
self.clear()
|
||||
self.widgets = defaultdict(dict)
|
||||
self.grid = []
|
||||
theme = self.config.theme
|
||||
self.config = FigureConfig(
|
||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||
)
|
||||
|
||||
def start(self):
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = QMainWindow()
|
||||
win.setCentralWidget(self)
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
##################################################
|
||||
##################################################
|
||||
# Debug window
|
||||
##################################################
|
||||
##################################################
|
||||
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
|
||||
|
||||
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.kernel_manager = QtInProcessKernelManager()
|
||||
self.kernel_manager.start_kernel(show_banner=False)
|
||||
self.kernel_client = self.kernel_manager.client()
|
||||
self.kernel_client.start_channels()
|
||||
|
||||
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class DebugWindow(QWidget): # pragma: no cover:
|
||||
"""Debug window for BEC widgets"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "figure_debug_minimal.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
self.splitter.setSizes([200, 100])
|
||||
|
||||
# console push
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{"fig": self.figure, "w1": self.w1, "w2": self.w2}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
|
||||
self.figure = BECFigure(parent=self) # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.widget_console)
|
||||
self.console = JupyterConsoleWidget()
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.console.set_default_style("linux")
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=0, col=0, title="Widget 1")
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=1, col=0, title="Widget 2")
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=0, col=1, title="Widget 3")
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=1, col=1, title="Widget 4")
|
||||
|
||||
self.w1 = self.figure[0, 0]
|
||||
self.w2 = self.figure[1, 0]
|
||||
self.w3 = self.figure[0, 1]
|
||||
self.w4 = self.figure[1, 1]
|
||||
|
||||
# curves for w1
|
||||
self.w1.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
self.w1.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5],
|
||||
y=[1, 2, 3, 4, 5],
|
||||
label="curve-custom",
|
||||
color="blue",
|
||||
pen_style="dashdot",
|
||||
)
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
# curves for w2
|
||||
self.w2.add_curve_scan("samx", "bpm3a", pen_style="solid")
|
||||
self.w2.add_curve_scan("samx", "bpm4d", pen_style="dot")
|
||||
self.w2.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5], y=[5, 4, 3, 2, 1], color="red", pen_style="dashdot"
|
||||
)
|
||||
|
||||
# curves for w3
|
||||
self.w3.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
self.w3.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5],
|
||||
y=[1, 2, 3, 4, 5],
|
||||
label="curve-custom",
|
||||
color="blue",
|
||||
pen_style="dashdot",
|
||||
)
|
||||
|
||||
# curves for w4
|
||||
self.w4.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
self.w4.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5],
|
||||
y=[1, 2, 3, 4, 5],
|
||||
label="curve-custom",
|
||||
color="blue",
|
||||
pen_style="dashdot",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = DebugWindow()
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
35
bec_widgets/widgets/figure/figure_debug_minimal.ui
Normal file
35
bec_widgets/widgets/figure/figure_debug_minimal.ui
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>901</width>
|
||||
<height>1000</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="glw" native="true"/>
|
||||
</widget>
|
||||
<widget class="QWidget" name="widget_console" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCAN_MODE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "gauss_bpm"}],
|
||||
"y": [{"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -47,7 +47,7 @@ CONFIG_SCAN_MODE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "gauss_adc1"}],
|
||||
"y": [{"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -61,7 +61,7 @@ CONFIG_SCAN_MODE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samy"}],
|
||||
"y": [{"name": "gauss_adc2"}],
|
||||
"y": [{"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -75,7 +75,7 @@ CONFIG_SCAN_MODE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samy", "entry": "samy"}],
|
||||
"y": [{"name": "gauss_adc3"}],
|
||||
"y": [{"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -105,7 +105,7 @@ CONFIG_SCAN_MODE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
|
||||
"y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -228,7 +228,7 @@ CONFIG_SIMPLE = {
|
||||
"type": "scan_segment",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
|
||||
"y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
7
bec_widgets/widgets/motor_control/__init__.py
Normal file
7
bec_widgets/widgets/motor_control/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .motor_control import (
|
||||
MotorControlRelative,
|
||||
MotorControlAbsolute,
|
||||
MotorControlSelection,
|
||||
MotorThread,
|
||||
MotorCoordinateTable,
|
||||
)
|
||||
1194
bec_widgets/widgets/motor_control/motor_control.py
Normal file
1194
bec_widgets/widgets/motor_control/motor_control.py
Normal file
File diff suppressed because it is too large
Load Diff
149
bec_widgets/widgets/motor_control/motor_control_absolute.ui
Normal file
149
bec_widgets/widgets/motor_control/motor_control_absolute.ui
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Move Movement Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl_absolute">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Move Movement Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_save_with_go">
|
||||
<property name="text">
|
||||
<string>Save position with Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_save">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_set">
|
||||
<property name="text">
|
||||
<string>Set</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_go_absolute">
|
||||
<property name="text">
|
||||
<string>Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
298
bec_widgets/widgets/motor_control/motor_control_relative.ui
Normal file
298
bec_widgets/widgets/motor_control/motor_control_relative.ui
Normal file
@@ -0,0 +1,298 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>405</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>405</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>394</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Control Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_enableArrows">
|
||||
<property name="text">
|
||||
<string>Move with arrow keys</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_same_xy">
|
||||
<property name="text">
|
||||
<string>Step [X] = Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="step_grid">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Decimal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [X]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_precision">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="direction_grid">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_up">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::UpArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_down">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::DownArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QToolButton" name="toolButton_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::LeftArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="toolButton_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::RightArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
69
bec_widgets/widgets/motor_control/motor_control_selection.ui
Normal file
69
bec_widgets/widgets/motor_control/motor_control_selection.ui
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>156</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>156</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Selection</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorSelection">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>145</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Selection</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Motor X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_x"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Motor Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="pushButton_connecMotors">
|
||||
<property name="text">
|
||||
<string>Connect Motors</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_y"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
113
bec_widgets/widgets/motor_control/motor_control_table.ui
Normal file
113
bec_widgets/widgets/motor_control/motor_control_table.ui
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>676</width>
|
||||
<height>667</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Coordinates Table</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_resize_auto">
|
||||
<property name="text">
|
||||
<string>Resize Auto</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_editColumns">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit Custom Column</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Entries Mode:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Individual</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Start/Stop</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="table">
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::SolidLine</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_importCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_exportCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -122,6 +122,28 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
else: # TODO implement validator
|
||||
print("Do validation")
|
||||
|
||||
@pyqtSlot(str, str, int)
|
||||
def change_motors(self, motor_x: str, motor_y: str, subplot: int = 0) -> 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.
|
||||
subplot(int): Subplot number.
|
||||
"""
|
||||
if subplot >= len(self.plot_data):
|
||||
print(f"Invalid subplot index: {subplot}. Available subplots: {len(self.plot_data)}")
|
||||
return
|
||||
|
||||
# Update the motor names in the plot configuration
|
||||
self.config["motors"][subplot]["signals"]["x"][0]["name"] = motor_x
|
||||
self.config["motors"][subplot]["signals"]["x"][0]["entry"] = motor_x
|
||||
self.config["motors"][subplot]["signals"]["y"][0]["name"] = motor_y
|
||||
self.config["motors"][subplot]["signals"]["y"][0]["entry"] = motor_y
|
||||
|
||||
# reinitialise the config and UI
|
||||
self._init_config()
|
||||
|
||||
def _init_config(self):
|
||||
"""Initiate the configuration."""
|
||||
|
||||
@@ -146,6 +168,9 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
# Connect motors to slots
|
||||
self._connect_motors_to_slots()
|
||||
|
||||
# Render init position of selected motors
|
||||
self._update_plots()
|
||||
|
||||
def _get_global_settings(self):
|
||||
"""Get global settings from the config."""
|
||||
self.plot_settings = self.config.get("plot_settings", {})
|
||||
@@ -488,6 +513,32 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
self.curves_data[plot_name]["highlight_V"].setPos(current_x)
|
||||
self.curves_data[plot_name]["highlight_H"].setPos(current_y)
|
||||
|
||||
@pyqtSlot(list, str, str)
|
||||
def plot_saved_coordinates(self, coordinates: list, tag: str, color: str):
|
||||
"""
|
||||
Plot saved coordinates on the map.
|
||||
Args:
|
||||
coordinates(list): List of coordinates to be plotted.
|
||||
tag(str): Tag for the coordinates for future reference.
|
||||
color(str): Color to plot coordinates in.
|
||||
"""
|
||||
for plot_name in self.plots:
|
||||
plot = self.plots[plot_name]
|
||||
|
||||
# Clear previous saved points
|
||||
if tag in self.curves_data[plot_name]:
|
||||
plot.removeItem(self.curves_data[plot_name][tag])
|
||||
|
||||
# Filter coordinates to be shown
|
||||
visible_coords = [coord[:2] for coord in coordinates if coord[2]]
|
||||
|
||||
if visible_coords:
|
||||
saved_points = pg.ScatterPlotItem(
|
||||
pos=np.array(visible_coords), brush=pg.mkBrush(color)
|
||||
)
|
||||
plot.addItem(saved_points)
|
||||
self.curves_data[plot_name][tag] = saved_points
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_device_readback(self, msg: dict):
|
||||
"""
|
||||
|
||||
2
bec_widgets/widgets/plots/__init__.py
Normal file
2
bec_widgets/widgets/plots/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .plot_base import AxisConfig, WidgetConfig, BECPlotBase
|
||||
from .waveform1d import Waveform1DConfig, BECWaveform1D, BECCurve
|
||||
238
bec_widgets/widgets/plots/plot_base.py
Normal file
238
bec_widgets/widgets/plots/plot_base.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
title: Optional[str] = Field(None, description="The title of the axes.")
|
||||
x_label: Optional[str] = Field(None, description="The label for the x-axis.")
|
||||
y_label: Optional[str] = Field(None, description="The label for the y-axis.")
|
||||
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.")
|
||||
|
||||
|
||||
class WidgetConfig(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 BECPlotBase(BECConnector, pg.PlotItem):
|
||||
USER_ACCESS = [
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"plot_data",
|
||||
"remove",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None, # TODO decide if needed for this class
|
||||
parent_figure=None,
|
||||
config: Optional[WidgetConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = WidgetConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
pg.PlotItem.__init__(self, parent)
|
||||
|
||||
self.figure = parent_figure
|
||||
|
||||
self.add_legend()
|
||||
|
||||
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
|
||||
"""
|
||||
# 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,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
print(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_title(self, title: str):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
"""
|
||||
self.setTitle(title)
|
||||
self.config.axis.title = title
|
||||
|
||||
def set_x_label(self, label: str):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
"""
|
||||
self.setLabel("bottom", label)
|
||||
self.config.axis.x_label = label
|
||||
|
||||
def set_y_label(self, label: str):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
"""
|
||||
self.setLabel("left", label)
|
||||
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.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.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.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.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.showGrid(x, y)
|
||||
self.config.axis.x_grid = x
|
||||
self.config.axis.y_grid = y
|
||||
|
||||
def add_legend(self):
|
||||
self.addLegend()
|
||||
|
||||
def plot_data(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.
|
||||
"""
|
||||
# TODO very basic so far, add more options
|
||||
# TODO decide name of the method
|
||||
self.plot(data_x, data_y, **kwargs)
|
||||
|
||||
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(self):
|
||||
"""Cleanup the plot widget."""
|
||||
731
bec_widgets/widgets/plots/waveform1d.py
Normal file
731
bec_widgets/widgets/plots/waveform1d.py
Normal file
@@ -0,0 +1,731 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional, Any
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic import Field, BaseModel, ValidationError
|
||||
from pyqtgraph import mkBrush
|
||||
from qtpy import QtCore
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_lib import MessageEndpoints
|
||||
from bec_lib.scan_data import ScanData
|
||||
from bec_widgets.utils import Colors, ConnectionConfig, BECConnector, EntryValidator
|
||||
from bec_widgets.widgets.plots import BECPlotBase, WidgetConfig
|
||||
|
||||
|
||||
class SignalData(BaseModel):
|
||||
"""The data configuration of a signal in the 1D waveform widget for x and y axis."""
|
||||
|
||||
# TODO add validator on name and entry
|
||||
name: str
|
||||
entry: str
|
||||
unit: Optional[str] = None # todo implement later
|
||||
modifier: Optional[str] = None # todo implement later
|
||||
|
||||
|
||||
class Signal(BaseModel):
|
||||
"""The configuration of a signal in the 1D waveform widget."""
|
||||
|
||||
source: str # TODO add validator on the source type
|
||||
x: SignalData
|
||||
y: SignalData
|
||||
|
||||
|
||||
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[Any] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str] = Field(None, description="The color of the symbol of the curve.")
|
||||
symbol_size: Optional[int] = Field(5, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(2, 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."
|
||||
) # TODO here on or curve??
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
|
||||
|
||||
class Waveform1DConfig(WidgetConfig):
|
||||
color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
|
||||
"plasma", description="The color palette of the figure widget."
|
||||
)
|
||||
curves: dict[str, CurveConfig] = Field(
|
||||
{}, description="The list of curves to be added to the 1D waveform widget."
|
||||
)
|
||||
|
||||
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = 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)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.apply_config()
|
||||
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 = mkBrush(color=symbol_color)
|
||||
self.setSymbolBrush(brush)
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
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,
|
||||
"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:
|
||||
print(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.apply_config()
|
||||
|
||||
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 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.
|
||||
"""
|
||||
x_data, y_data = self.getData()
|
||||
return x_data, y_data
|
||||
|
||||
|
||||
class BECWaveform1D(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"add_curve_scan",
|
||||
"add_curve_custom",
|
||||
"remove_curve",
|
||||
"scan_history",
|
||||
"curves",
|
||||
"curves_data",
|
||||
"get_curve",
|
||||
"get_curve_config",
|
||||
"apply_config",
|
||||
"get_all_data",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[Waveform1DConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
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
|
||||
)
|
||||
|
||||
self.curves_data = defaultdict(dict)
|
||||
self.scanID = None
|
||||
|
||||
# Scan segment update proxy
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.scan_signal_update, rateLimit=25, slot=self._update_scan_segment_plot
|
||||
)
|
||||
|
||||
# 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())
|
||||
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.addLegend()
|
||||
self.apply_config(self.config)
|
||||
|
||||
# TODO check config assigning
|
||||
# TODO check the functionality of config generator
|
||||
def find_widget_by_id(
|
||||
self, item_id: str
|
||||
): # TODO implement this on level of BECConnector and all other widgets
|
||||
for curve in self.curves:
|
||||
if curve.gui_id == item_id:
|
||||
return curve
|
||||
|
||||
def apply_config(self, config: dict | WidgetConfig, replot_last_scan: bool = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
Args:
|
||||
config(dict|WidgetConfig): 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:
|
||||
print(f"Validation error when applying config to BECWaveform1D: {e}")
|
||||
return
|
||||
|
||||
self.config = config
|
||||
self.clear()
|
||||
|
||||
self.apply_axis_config()
|
||||
# Reset curves
|
||||
self._curves_data = defaultdict(dict)
|
||||
self._curves = []
|
||||
for curve_id, curve_config in self.config.curves.items():
|
||||
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
|
||||
|
||||
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 source, curves in self.curves_data.items():
|
||||
if curve_id in curves:
|
||||
if dict_output:
|
||||
return curves[curve_id].config.model_dump()
|
||||
else:
|
||||
return curves[curve_id].config
|
||||
|
||||
@property
|
||||
def curves(self) -> list[BECCurve]: # TODO discuss if it should be marked as @property for RPC
|
||||
"""
|
||||
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 curves_data(self) -> dict: # TODO discuss if it should be marked as @property for RPC
|
||||
"""
|
||||
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):
|
||||
self._curves_data = value
|
||||
|
||||
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.curves[identifier]
|
||||
elif isinstance(identifier, str):
|
||||
for source_type, curves in self.curves_data.items():
|
||||
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 add_curve_custom(
|
||||
self,
|
||||
x: list | np.ndarray,
|
||||
y: list | np.ndarray,
|
||||
label: str = None,
|
||||
color: str = None,
|
||||
**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.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve_source = "custom"
|
||||
curve_id = label or f"Curve {len(self.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=len(self.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
)
|
||||
|
||||
# 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_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)
|
||||
self.curves_data[source][name] = curve
|
||||
self.addItem(curve)
|
||||
self.config.curves[name] = curve.config
|
||||
if data is not None:
|
||||
curve.setData(data[0], data[1])
|
||||
return curve
|
||||
|
||||
def add_curve_scan(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
x_entry: Optional[str] = None,
|
||||
y_entry: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
validate_bec: bool = True,
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve to the plot widget from the scan segment.
|
||||
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.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
# Check if curve already exists
|
||||
curve_source = "scan_segment"
|
||||
|
||||
# Get entry if not provided and validate
|
||||
x_entry, y_entry = self._validate_signal_entries(
|
||||
x_name, y_name, x_entry, y_entry, validate_bec
|
||||
)
|
||||
|
||||
label = label or f"{y_name}-{y_entry}"
|
||||
|
||||
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}'.")
|
||||
|
||||
color = (
|
||||
color
|
||||
or Colors.golden_angle_color(
|
||||
colormap=self.config.color_palette, num=len(self.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
)
|
||||
|
||||
# Create curve by config
|
||||
curve_config = CurveConfig(
|
||||
widget_class="BECCurve",
|
||||
parent_id=self.gui_id,
|
||||
label=label,
|
||||
color=color,
|
||||
source=curve_source,
|
||||
signals=Signal(
|
||||
source=curve_source,
|
||||
x=SignalData(name=x_name, entry=x_entry),
|
||||
y=SignalData(name=y_name, entry=y_entry),
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
|
||||
return curve
|
||||
|
||||
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 _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 source, curves in self.curves_data.items():
|
||||
if curve_id in curves:
|
||||
curve = curves.pop(curve_id)
|
||||
self.removeItem(curve)
|
||||
del self.config.curves[curve_id]
|
||||
if curve in self.curves:
|
||||
self.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.curves):
|
||||
curve = self.curves[N]
|
||||
curve_id = curve.name() # Assuming curve's name is used as its ID
|
||||
self.removeItem(curve)
|
||||
del self.config.curves[curve_id]
|
||||
# Remove from self.curve_data
|
||||
for source, curves in self.curves_data.items():
|
||||
if curve_id in curves:
|
||||
del curves[curve_id]
|
||||
break
|
||||
else:
|
||||
raise IndexError(f"Curve order {N} out of range.")
|
||||
|
||||
@pyqtSlot(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.
|
||||
|
||||
Args:
|
||||
msg (dict): Message received with scan data.
|
||||
metadata (dict): Metadata of the scan.
|
||||
"""
|
||||
current_scanID = msg.get("scanID", None)
|
||||
if current_scanID is None:
|
||||
return
|
||||
|
||||
if current_scanID != self.scanID:
|
||||
self.scanID = current_scanID
|
||||
self.scan_segment_data = self.queue.scan_storage.find_scan_by_ID(
|
||||
self.scanID
|
||||
) # TODO do scan access through BECFigure
|
||||
|
||||
self.scan_signal_update.emit()
|
||||
|
||||
def _update_scan_segment_plot(self):
|
||||
"""Update the plot with the data from the scan segment."""
|
||||
data = self.scan_segment_data.data
|
||||
self._update_scan_curves(data)
|
||||
|
||||
def _update_scan_curves(self, data: ScanData):
|
||||
"""
|
||||
Update the scan curves with the data from the scan segment.
|
||||
Args:
|
||||
data(ScanData): Data from the scan segment.
|
||||
"""
|
||||
for curve_id, curve in self.curves_data["scan_segment"].items():
|
||||
x_name = curve.config.signals.x.name
|
||||
x_entry = curve.config.signals.x.entry
|
||||
y_name = curve.config.signals.y.name
|
||||
y_entry = curve.config.signals.y.entry
|
||||
|
||||
try:
|
||||
data_x = data[x_name][x_entry].val
|
||||
data_y = data[y_name][y_entry].val
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
curve.setData(data_x, data_y)
|
||||
|
||||
def scan_history(self, scan_index: int = None, scanID: str = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scanID or scan_index.
|
||||
Args:
|
||||
scanID(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 scanID is not None:
|
||||
raise ValueError("Only one of scanID or scan_index can be provided.")
|
||||
|
||||
if scan_index is not None:
|
||||
self.scanID = self.queue.scan_storage.storage[scan_index].scanID
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scanID).data
|
||||
elif scanID is not None:
|
||||
self.scanID = scanID
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scanID).data
|
||||
|
||||
self._update_scan_curves(data)
|
||||
|
||||
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":
|
||||
print(
|
||||
"Pandas is not installed. "
|
||||
"Please install pandas using 'pip install pandas'."
|
||||
"Output will be dictionary instead."
|
||||
)
|
||||
output = "dict"
|
||||
|
||||
for curve in self.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.curves],
|
||||
axis=1,
|
||||
keys=[curve.name() for curve in self.curves],
|
||||
)
|
||||
return combined_data
|
||||
return data
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget connection from BECDispatcher."""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
@@ -119,8 +119,7 @@ class ScanControl(QWidget):
|
||||
|
||||
def populate_scans(self):
|
||||
"""Populates the scan selection combo box with available scans"""
|
||||
msg = self.client.producer.get(MessageEndpoints.available_scans())
|
||||
self.available_scans = msgpack.loads(msg)
|
||||
self.available_scans = self.client.producer.get(MessageEndpoints.available_scans()).resource
|
||||
if self.allowed_scans is None:
|
||||
allowed_scans = self.available_scans.keys()
|
||||
else:
|
||||
|
||||
16
setup.py
16
setup.py
@@ -1,7 +1,7 @@
|
||||
# pylint: disable= missing-module-docstring
|
||||
from setuptools import setup
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
__version__ = "0.38.1"
|
||||
__version__ = "0.41.1"
|
||||
|
||||
# Default to PyQt6 if no other Qt binding is installed
|
||||
QT_DEPENDENCY = "PyQt6>=6.0"
|
||||
@@ -32,9 +32,19 @@ if __name__ == "__main__":
|
||||
"pyqtdarktheme",
|
||||
],
|
||||
extras_require={
|
||||
"dev": ["pytest", "pytest-random-order", "coverage", "pytest-qt", "black"],
|
||||
"dev": [
|
||||
"pytest",
|
||||
"pytest-random-order",
|
||||
"pytest-timeout",
|
||||
"coverage",
|
||||
"pytest-qt",
|
||||
"black",
|
||||
],
|
||||
"pyqt5": ["PyQt5>=5.9"],
|
||||
"pyqt6": ["PyQt6>=6.0"],
|
||||
},
|
||||
version=__version__,
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
package_data={"": ["*.ui", "*.yaml"]},
|
||||
)
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
77
tests/client_mocks.py
Normal file
77
tests/client_mocks.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
class FakeDevice:
|
||||
"""Fake minimal positioner class for testing."""
|
||||
|
||||
def __init__(self, name, enabled=True):
|
||||
self.name = name
|
||||
self.enabled = enabled
|
||||
self.signals = {self.name: {"value": 1.0}}
|
||||
self.description = {self.name: {"source": self.name}}
|
||||
|
||||
def __contains__(self, item):
|
||||
return item == self.name
|
||||
|
||||
@property
|
||||
def _hints(self):
|
||||
return [self.name]
|
||||
|
||||
def set_value(self, fake_value: float = 1.0) -> None:
|
||||
"""
|
||||
Setup fake value for device readout
|
||||
Args:
|
||||
fake_value(float): Desired fake value
|
||||
"""
|
||||
self.signals[self.name]["value"] = fake_value
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""
|
||||
Get the description of the device
|
||||
Returns:
|
||||
dict: Description of the device
|
||||
"""
|
||||
return self.description
|
||||
|
||||
|
||||
def get_mocked_device(device_name: str):
|
||||
"""
|
||||
Helper function to mock the devices
|
||||
Args:
|
||||
device_name(str): Name of the device to mock
|
||||
"""
|
||||
return FakeDevice(name=device_name, enabled=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client():
|
||||
# Create a dictionary of mocked devices
|
||||
device_names = [
|
||||
"samx",
|
||||
"samy",
|
||||
"gauss_bpm",
|
||||
"gauss_adc1",
|
||||
"gauss_adc2",
|
||||
"gauss_adc3",
|
||||
"bpm4i",
|
||||
"bpm3a",
|
||||
"bpm3i",
|
||||
]
|
||||
mocked_devices = {name: get_mocked_device(name) for name in device_names}
|
||||
|
||||
# Create a MagicMock object
|
||||
client = MagicMock()
|
||||
|
||||
# Mock the device_manager.devices attribute
|
||||
client.device_manager.devices = MagicMock()
|
||||
client.device_manager.devices.__getitem__.side_effect = lambda x: mocked_devices.get(x)
|
||||
client.device_manager.devices.__contains__.side_effect = lambda x: x in mocked_devices
|
||||
|
||||
# Set each device as an attribute of the mock
|
||||
for name, device in mocked_devices.items():
|
||||
setattr(client.device_manager.devices, name, device)
|
||||
|
||||
return client
|
||||
56
tests/test_bec_connector.py
Normal file
56
tests/test_bec_connector.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import pytest
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_connector(mocked_client):
|
||||
connector = BECConnector(client=mocked_client)
|
||||
return connector
|
||||
|
||||
|
||||
def test_bec_connector_init(bec_connector):
|
||||
assert bec_connector is not None
|
||||
assert bec_connector.client is not None
|
||||
assert isinstance(bec_connector, BECConnector)
|
||||
assert bec_connector.config.widget_class == "BECConnector"
|
||||
|
||||
|
||||
def test_bec_connector_init_with_gui_id(mocked_client):
|
||||
bc = BECConnector(client=mocked_client, gui_id="test_gui_id")
|
||||
assert bc.config.gui_id == "test_gui_id"
|
||||
assert bc.gui_id == "test_gui_id"
|
||||
|
||||
|
||||
def test_bec_connector_set_gui_id(bec_connector):
|
||||
bec_connector.set_gui_id("test_gui_id")
|
||||
assert bec_connector.config.gui_id == "test_gui_id"
|
||||
|
||||
|
||||
def test_bec_connector_change_config(bec_connector):
|
||||
bec_connector.on_config_update({"gui_id": "test_gui_id"})
|
||||
assert bec_connector.config.gui_id == "test_gui_id"
|
||||
|
||||
|
||||
def test_bec_connector_get_obj_by_id(bec_connector):
|
||||
bec_connector.set_gui_id("test_gui_id")
|
||||
assert bec_connector.get_obj_by_id("test_gui_id") == bec_connector
|
||||
assert bec_connector.get_obj_by_id("test_gui_id_2") is None
|
||||
|
||||
|
||||
def test_bec_connector_update_client(bec_connector, mocked_client):
|
||||
client_new = mocked_client
|
||||
bec_connector.update_client(client_new)
|
||||
assert bec_connector.client == client_new
|
||||
assert bec_connector.dev is not None
|
||||
assert bec_connector.scans is not None
|
||||
assert bec_connector.queue is not None
|
||||
assert bec_connector.scan_storage is not None
|
||||
assert bec_connector.dap is not None
|
||||
|
||||
|
||||
def test_bec_connector_get_config(bec_connector):
|
||||
assert bec_connector.get_config(dict_output=False) == bec_connector.config
|
||||
assert bec_connector.get_config() == bec_connector.config.model_dump()
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=missing-function-docstring
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
@@ -6,25 +6,24 @@ from bec_lib.messages import ScanMessage
|
||||
from bec_lib.connector import MessageObject
|
||||
|
||||
|
||||
msg = MessageObject(topic="", value=ScanMessage(point_id=0, scanID=0, data={}).dumps())
|
||||
msg = MessageObject(topic="", value=ScanMessage(point_id=0, scanID=0, data={}))
|
||||
|
||||
|
||||
@pytest.fixture(name="consumer")
|
||||
def _consumer(bec_dispatcher):
|
||||
bec_dispatcher.client.connector.consumer = Mock()
|
||||
consumer = bec_dispatcher.client.connector.consumer
|
||||
yield consumer
|
||||
bec_dispatcher.client.connector = Mock()
|
||||
yield bec_dispatcher.client.connector
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Failed to connect to redis.")
|
||||
def test_connect_one_slot(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.assert_called_once()
|
||||
consumer.register.assert_called_once()
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
|
||||
@@ -32,23 +31,23 @@ def test_connect_identical(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.assert_called_once()
|
||||
consumer.register.assert_called_once()
|
||||
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
|
||||
|
||||
def test_connect_many_slots_one_topic(bec_dispatcher, consumer):
|
||||
slot1, slot2 = Mock(), Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.assert_called_once()
|
||||
consumer.register.assert_called_once()
|
||||
bec_dispatcher.connect_slot(slot=slot2, topics="topic0")
|
||||
consumer.assert_called_once()
|
||||
consumer.register.assert_called_once()
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
slot2.assert_called_once()
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 2
|
||||
|
||||
@@ -56,13 +55,13 @@ def test_connect_many_slots_one_topic(bec_dispatcher, consumer):
|
||||
def test_connect_one_slot_many_topics(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
assert consumer.call_count == 1
|
||||
assert consumer.register.call_count == 1
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic1")
|
||||
assert consumer.call_count == 2
|
||||
assert consumer.register.call_count == 2
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
|
||||
@@ -72,19 +71,19 @@ def test_disconnect_one_slot_one_topic(bec_dispatcher, consumer):
|
||||
|
||||
# disconnect using a different topic
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic1")
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot2, topics="topic0")
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
# reset count to 0 for slot
|
||||
# reset count to for slot
|
||||
slot1.reset_mock()
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 0
|
||||
|
||||
|
||||
@@ -95,14 +94,14 @@ def test_disconnect_identical(bec_dispatcher, consumer):
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
|
||||
# Test to call the slot once (slot should be not connected twice)
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
# Disconnect the slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
|
||||
# Test to call the slot once (slot should be not connected anymore), count remains 1
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
|
||||
@@ -113,19 +112,19 @@ def test_disconnect_many_slots_one_topic(bec_dispatcher, consumer):
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot3, topics="topic0")
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
assert slot2.call_count == 1
|
||||
|
||||
# disconnect using a different topics
|
||||
bec_dispatcher.disconnect_slot(slot1, topics="topic1")
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 2
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot1, topics="topic0")
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 3
|
||||
|
||||
@@ -137,31 +136,31 @@ def test_disconnect_one_slot_many_topics(bec_dispatcher, consumer):
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot2, topics="topic0")
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
# disconnect using a different topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic3")
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 3
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 4
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
# Calling disconnected topic0 should not call slot1
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 4
|
||||
# Calling topic1 should still call slot1
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 5
|
||||
|
||||
# disconnect remaining topic1 from slot1, calling any topic should not increase count
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic1")
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 5
|
||||
|
||||
|
||||
@@ -178,9 +177,9 @@ def test_disconnect_all(bec_dispatcher, consumer):
|
||||
bec_dispatcher.disconnect_all()
|
||||
|
||||
# Simulate messages and verify that none of the slots are called
|
||||
consumer.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.call_args_list[2].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[2].kwargs["cb"](msg)
|
||||
|
||||
# Ensure that the slots have not been called
|
||||
assert slot1.call_count == 0
|
||||
@@ -209,13 +208,13 @@ def test_connect_one_slot_multiple_topics_single_callback(bec_dispatcher, consum
|
||||
msg_with_topic = MessageObject(
|
||||
topic=topic, value=ScanMessage(point_id=0, scanID=0, data={}).dumps()
|
||||
)
|
||||
consumer.call_args.kwargs["cb"](msg_with_topic)
|
||||
consumer.register.call_args.kwargs["cb"](msg_with_topic)
|
||||
|
||||
# Verify that the slot is called once for each topic
|
||||
assert slot1.call_count == len(topics)
|
||||
|
||||
# Verify that a single consumer is created for all topics
|
||||
consumer.assert_called_once()
|
||||
consumer.register.assert_called_once()
|
||||
|
||||
|
||||
def test_disconnect_all_with_single_callback_for_multiple_topics(bec_dispatcher, consumer):
|
||||
@@ -237,5 +236,5 @@ def test_disconnect_all_with_single_callback_for_multiple_topics(bec_dispatcher,
|
||||
assert slot1.call_count == 0 # Slot has not been called
|
||||
|
||||
# Simulate messages and verify that the slot is not called
|
||||
consumer.call_args.kwargs["cb"](msg)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 0 # Slot has not been called
|
||||
|
||||
226
tests/test_bec_figure.py
Normal file
226
tests/test_bec_figure.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from bec_widgets.widgets import BECFigure
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_figure(qtbot, mocked_client):
|
||||
widget = BECFigure(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def test_bec_figure_init(bec_figure):
|
||||
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(bec_figure):
|
||||
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")
|
||||
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
|
||||
|
||||
def test_access_widgets_access_errors(bec_figure):
|
||||
bec_figure.add_plot(widget_id="test_waveform_1", 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(bec_figure):
|
||||
bec_figure.add_plot(widget_id="test_waveform_1", row=0, col=0)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bec_figure.add_plot(widget_id="test_waveform_2", 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)
|
||||
|
||||
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 "test_waveform_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
|
||||
|
||||
# remove by widget object
|
||||
w3.remove()
|
||||
assert "test_waveform_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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
bec_figure.remove(0, 0)
|
||||
assert "test_waveform_1" not in bec_figure.widgets
|
||||
assert "test_waveform_2" in bec_figure.widgets
|
||||
assert bec_figure[0, 0] == w2
|
||||
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)
|
||||
|
||||
bec_figure.remove(coordinates=(0, 0))
|
||||
assert "test_waveform_1" not in bec_figure.widgets
|
||||
assert "test_waveform_2" in bec_figure.widgets
|
||||
assert bec_figure[0, 0] == w2
|
||||
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)
|
||||
|
||||
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(bec_figure):
|
||||
bec_figure.add_plot(widget_id="test_waveform_1", 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(bec_figure):
|
||||
bec_figure.add_plot(widget_id="test_waveform_1", 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):
|
||||
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(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)
|
||||
|
||||
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(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.clear_all()
|
||||
|
||||
assert len(bec_figure.widgets) == 0
|
||||
assert np.shape(bec_figure.grid) == (0,)
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import os
|
||||
import yaml
|
||||
|
||||
@@ -79,7 +80,7 @@ def mocked_client():
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def monitor(qtbot, mocked_client):
|
||||
def monitor(bec_dispatcher, qtbot, mocked_client):
|
||||
# client = MagicMock()
|
||||
widget = BECMonitor(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
import numpy as np
|
||||
|
||||
649
tests/test_motor_control.py
Normal file
649
tests/test_motor_control.py
Normal file
@@ -0,0 +1,649 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from unittest.mock import patch
|
||||
from bec_lib.device import Positioner
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from bec_widgets.widgets import (
|
||||
MotorControlSelection,
|
||||
MotorControlAbsolute,
|
||||
MotorControlRelative,
|
||||
MotorThread,
|
||||
MotorCoordinateTable,
|
||||
)
|
||||
from bec_widgets.examples import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
)
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorActions
|
||||
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"motor_control": {
|
||||
"motor_x": "samx",
|
||||
"motor_y": "samy",
|
||||
"step_size_x": 3,
|
||||
"step_size_y": 3,
|
||||
"precision": 4,
|
||||
"step_x_y_same": False,
|
||||
"move_with_arrows": False,
|
||||
},
|
||||
"plot_settings": {
|
||||
"colormap": "Greys",
|
||||
"scatter_size": 5,
|
||||
"max_points": 1000,
|
||||
"num_dim_points": 100,
|
||||
"precision": 2,
|
||||
"num_columns": 1,
|
||||
"background_value": 25,
|
||||
},
|
||||
"motors": [
|
||||
{
|
||||
"plot_name": "Motor Map",
|
||||
"x_label": "Motor X",
|
||||
"y_label": "Motor Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "samy", "entry": "samy"}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
#######################################################
|
||||
# Client and devices fixture
|
||||
#######################################################
|
||||
|
||||
|
||||
class FakeDevice:
|
||||
"""Fake minimal positioner class for testing."""
|
||||
|
||||
def __init__(self, name, enabled=True, limits=None, read_value=1.0):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.enabled = enabled
|
||||
self.read_value = read_value
|
||||
self.limits = limits or (-100, 100) # Default limits if not provided
|
||||
|
||||
def read(self):
|
||||
"""Simulates reading the current position of the device."""
|
||||
return {self.name: {"value": self.read_value}}
|
||||
|
||||
def move(self, value, relative=False):
|
||||
"""Simulates moving the device to a new position."""
|
||||
if relative:
|
||||
self.read_value += value
|
||||
else:
|
||||
self.read_value = value
|
||||
# Respect the limits
|
||||
self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
|
||||
|
||||
@property
|
||||
def readback(self):
|
||||
return MagicMock(get=MagicMock(return_value=self.read_value))
|
||||
|
||||
def describe(self):
|
||||
"""Describes the device."""
|
||||
return {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_client():
|
||||
client = MagicMock()
|
||||
|
||||
# Setup the fake devices
|
||||
motors = {
|
||||
"samx": FakeDevice("samx", limits=[-10, 10], read_value=2.0),
|
||||
"samy": FakeDevice("samy", limits=[-5, 5], read_value=3.0),
|
||||
"aptrx": FakeDevice("aptrx", read_value=4.0),
|
||||
"aptry": FakeDevice("aptry", read_value=5.0),
|
||||
}
|
||||
|
||||
client.device_manager.devices = MagicMock()
|
||||
client.device_manager.devices.__getitem__.side_effect = lambda x: motors.get(x, FakeDevice(x))
|
||||
client.device_manager.devices.enabled_devices = list(motors.values())
|
||||
|
||||
# Mock the scans.mv method
|
||||
def mock_mv(*args, relative=False):
|
||||
# Extracting motor and value pairs
|
||||
for i in range(0, len(args), 2):
|
||||
motor = args[i]
|
||||
value = args[i + 1]
|
||||
motor.move(value, relative=relative)
|
||||
return MagicMock(wait=MagicMock()) # Simulate wait method of the move status object
|
||||
|
||||
client.scans = MagicMock(mv=mock_mv)
|
||||
|
||||
# Ensure isinstance check for Positioner passes
|
||||
original_isinstance = isinstance
|
||||
|
||||
def isinstance_mock(obj, class_info):
|
||||
if class_info == Positioner:
|
||||
return True
|
||||
return original_isinstance(obj, class_info)
|
||||
|
||||
with patch("builtins.isinstance", new=isinstance_mock):
|
||||
yield client
|
||||
|
||||
|
||||
#######################################################
|
||||
# Motor Thread
|
||||
#######################################################
|
||||
@pytest.fixture
|
||||
def motor_thread(mocked_client):
|
||||
"""Fixture for MotorThread with a mocked client."""
|
||||
return MotorThread(client=mocked_client)
|
||||
|
||||
|
||||
def test_motor_thread_initialization(mocked_client):
|
||||
motor_thread = MotorThread(client=mocked_client)
|
||||
assert motor_thread.client == mocked_client
|
||||
assert isinstance(motor_thread.dev, MagicMock)
|
||||
|
||||
|
||||
def test_get_all_motors_names(mocked_client):
|
||||
motor_thread = MotorThread(client=mocked_client)
|
||||
motor_names = motor_thread.get_all_motors_names()
|
||||
expected_names = ["samx", "samy", "aptrx", "aptry"]
|
||||
assert sorted(motor_names) == sorted(expected_names)
|
||||
assert all(name in motor_names for name in expected_names)
|
||||
assert len(motor_names) == len(expected_names) # Ensure only these motors are returned
|
||||
|
||||
|
||||
def test_get_coordinates(mocked_client):
|
||||
motor_thread = MotorThread(client=mocked_client)
|
||||
motor_x, motor_y = "samx", "samy"
|
||||
x, y = motor_thread.get_coordinates(motor_x, motor_y)
|
||||
|
||||
assert x == mocked_client.device_manager.devices[motor_x].readback.get()
|
||||
assert y == mocked_client.device_manager.devices[motor_y].readback.get()
|
||||
|
||||
|
||||
def test_move_motor_absolute_by_run(mocked_client):
|
||||
motor_thread = MotorThread(client=mocked_client)
|
||||
motor_thread.motor_x = "samx"
|
||||
motor_thread.motor_y = "samy"
|
||||
motor_thread.target_coordinates = (5.0, -3.0)
|
||||
motor_thread.action = MotorActions.MOVE_ABSOLUTE
|
||||
motor_thread.run()
|
||||
|
||||
assert mocked_client.device_manager.devices["samx"].read_value == 5.0
|
||||
assert mocked_client.device_manager.devices["samy"].read_value == -3.0
|
||||
|
||||
|
||||
def test_move_motor_relative_by_run(mocked_client):
|
||||
motor_thread = MotorThread(client=mocked_client)
|
||||
motor_thread.motor = "samx"
|
||||
motor_thread.value = 2.0
|
||||
motor_thread.action = MotorActions.MOVE_RELATIVE
|
||||
motor_thread.run()
|
||||
|
||||
assert mocked_client.device_manager.devices["samx"].read_value == 4.0
|
||||
|
||||
|
||||
def test_motor_thread_move_absolute(motor_thread):
|
||||
motor_x = "samx"
|
||||
motor_y = "samy"
|
||||
target_x = 5.0
|
||||
target_y = -3.0
|
||||
|
||||
motor_thread.move_absolute(motor_x, motor_y, (target_x, target_y))
|
||||
motor_thread.wait()
|
||||
|
||||
assert motor_thread.dev[motor_x].read()["samx"]["value"] == target_x
|
||||
assert motor_thread.dev[motor_y].read()["samy"]["value"] == target_y
|
||||
|
||||
|
||||
def test_motor_thread_move_relative(motor_thread):
|
||||
motor_name = "samx"
|
||||
move_value = 2.0
|
||||
|
||||
initial_value = motor_thread.dev[motor_name].read()["samx"]["value"]
|
||||
motor_thread.move_relative(motor_name, move_value)
|
||||
motor_thread.wait()
|
||||
|
||||
expected_value = initial_value + move_value
|
||||
assert motor_thread.dev[motor_name].read()["samx"]["value"] == expected_value
|
||||
|
||||
|
||||
#######################################################
|
||||
# Motor Control Widgets - MotorControlSelection
|
||||
#######################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_selection_widget(qtbot, mocked_client, motor_thread):
|
||||
"""Fixture for creating a MotorControlSelection widget with a mocked client."""
|
||||
widget = MotorControlSelection(
|
||||
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
|
||||
)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def test_initialization_and_population(motor_selection_widget):
|
||||
assert motor_selection_widget.comboBox_motor_x.count() == 4
|
||||
assert motor_selection_widget.comboBox_motor_x.itemText(0) == "samx"
|
||||
assert motor_selection_widget.comboBox_motor_y.itemText(1) == "samy"
|
||||
assert motor_selection_widget.comboBox_motor_x.itemText(2) == "aptrx"
|
||||
assert motor_selection_widget.comboBox_motor_y.itemText(3) == "aptry"
|
||||
|
||||
|
||||
def test_selection_and_signal_emission(motor_selection_widget):
|
||||
# Connect signal to a custom slot to capture the emitted values
|
||||
emitted_values = []
|
||||
|
||||
def capture_emitted_values(motor_x, motor_y):
|
||||
emitted_values.append((motor_x, motor_y))
|
||||
|
||||
motor_selection_widget.selected_motors_signal.connect(capture_emitted_values)
|
||||
|
||||
# Select motors
|
||||
motor_selection_widget.comboBox_motor_x.setCurrentIndex(0) # Select 'samx'
|
||||
motor_selection_widget.comboBox_motor_y.setCurrentIndex(1) # Select 'samy'
|
||||
motor_selection_widget.pushButton_connecMotors.click() # Emit the signal
|
||||
|
||||
# Verify the emitted signal
|
||||
assert emitted_values == [
|
||||
("samx", "samy")
|
||||
], "The emitted signal did not match the expected values"
|
||||
|
||||
|
||||
def test_configuration_update(motor_selection_widget):
|
||||
new_config = {"motor_control": {"motor_x": "samy", "motor_y": "samx"}}
|
||||
motor_selection_widget.on_config_update(new_config)
|
||||
assert motor_selection_widget.comboBox_motor_x.currentText() == "samy"
|
||||
assert motor_selection_widget.comboBox_motor_y.currentText() == "samx"
|
||||
|
||||
|
||||
def test_enable_motor_controls(motor_selection_widget):
|
||||
motor_selection_widget.enable_motor_controls(False)
|
||||
assert not motor_selection_widget.comboBox_motor_x.isEnabled()
|
||||
assert not motor_selection_widget.comboBox_motor_y.isEnabled()
|
||||
|
||||
motor_selection_widget.enable_motor_controls(True)
|
||||
assert motor_selection_widget.comboBox_motor_x.isEnabled()
|
||||
assert motor_selection_widget.comboBox_motor_y.isEnabled()
|
||||
|
||||
|
||||
#######################################################
|
||||
# Motor Control Widgets - MotorControlAbsolute
|
||||
#######################################################
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_absolute_widget(qtbot, mocked_client, motor_thread):
|
||||
widget = MotorControlAbsolute(
|
||||
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
|
||||
)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def test_absolute_initialization(motor_absolute_widget):
|
||||
motor_absolute_widget.change_motors("samx", "samy")
|
||||
motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
|
||||
assert motor_absolute_widget.motor_x == "samx", "Motor X not initialized correctly"
|
||||
assert motor_absolute_widget.motor_y == "samy", "Motor Y not initialized correctly"
|
||||
assert motor_absolute_widget.precision == CONFIG_DEFAULT["motor_control"]["precision"]
|
||||
|
||||
|
||||
def test_absolute_save_current_coordinates(motor_absolute_widget):
|
||||
motor_absolute_widget.client.device_manager["samx"].set_value(2.0)
|
||||
motor_absolute_widget.client.device_manager["samy"].set_value(3.0)
|
||||
motor_absolute_widget.change_motors("samx", "samy")
|
||||
|
||||
emitted_coordinates = []
|
||||
|
||||
def capture_emit(x_y):
|
||||
emitted_coordinates.append(x_y)
|
||||
|
||||
motor_absolute_widget.coordinates_signal.connect(capture_emit)
|
||||
|
||||
# Trigger saving current coordinates
|
||||
motor_absolute_widget.pushButton_save.click()
|
||||
|
||||
# Default position of samx and samy are 2.0 and 3.0 respectively
|
||||
assert emitted_coordinates == [(2.0, 3.0)]
|
||||
|
||||
|
||||
def test_absolute_set_absolute_coordinates(motor_absolute_widget):
|
||||
motor_absolute_widget.spinBox_absolute_x.setValue(5)
|
||||
motor_absolute_widget.spinBox_absolute_y.setValue(10)
|
||||
|
||||
# Connect to the coordinates_signal to capture emitted values
|
||||
emitted_values = []
|
||||
|
||||
def capture_coordinates(x_y):
|
||||
emitted_values.append(x_y)
|
||||
|
||||
motor_absolute_widget.coordinates_signal.connect(capture_coordinates)
|
||||
|
||||
# Simulate button click for absolute movement
|
||||
motor_absolute_widget.pushButton_set.click()
|
||||
|
||||
assert emitted_values == [(5, 10)]
|
||||
|
||||
|
||||
def test_absolute_go_absolute_coordinates(motor_absolute_widget):
|
||||
motor_absolute_widget.change_motors("samx", "samy")
|
||||
|
||||
motor_absolute_widget.spinBox_absolute_x.setValue(5)
|
||||
motor_absolute_widget.spinBox_absolute_y.setValue(10)
|
||||
|
||||
with patch(
|
||||
"bec_widgets.widgets.motor_control.motor_control.MotorThread.move_absolute",
|
||||
new_callable=MagicMock,
|
||||
) as mock_move_absolute:
|
||||
motor_absolute_widget.pushButton_go_absolute.click()
|
||||
mock_move_absolute.assert_called_once_with("samx", "samy", (5, 10))
|
||||
|
||||
|
||||
def test_change_motor_absolute(motor_absolute_widget):
|
||||
motor_absolute_widget.change_motors("aptrx", "aptry")
|
||||
|
||||
assert motor_absolute_widget.motor_x == "aptrx"
|
||||
assert motor_absolute_widget.motor_y == "aptry"
|
||||
|
||||
motor_absolute_widget.change_motors("samx", "samy")
|
||||
|
||||
assert motor_absolute_widget.motor_x == "samx"
|
||||
assert motor_absolute_widget.motor_y == "samy"
|
||||
|
||||
|
||||
def test_set_precision(motor_absolute_widget):
|
||||
motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
|
||||
motor_absolute_widget.set_precision(2)
|
||||
|
||||
assert motor_absolute_widget.spinBox_absolute_x.decimals() == 2
|
||||
assert motor_absolute_widget.spinBox_absolute_y.decimals() == 2
|
||||
|
||||
|
||||
#######################################################
|
||||
# Motor Control Widgets - MotorControlRelative
|
||||
#######################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_relative_widget(qtbot, mocked_client, motor_thread):
|
||||
widget = MotorControlRelative(
|
||||
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
|
||||
)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def test_initialization_and_config_update(motor_relative_widget):
|
||||
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
|
||||
|
||||
assert motor_relative_widget.motor_x == CONFIG_DEFAULT["motor_control"]["motor_x"]
|
||||
assert motor_relative_widget.motor_y == CONFIG_DEFAULT["motor_control"]["motor_y"]
|
||||
assert motor_relative_widget.precision == CONFIG_DEFAULT["motor_control"]["precision"]
|
||||
|
||||
# Simulate a configuration update
|
||||
new_config = {
|
||||
"motor_control": {
|
||||
"motor_x": "new_motor_x",
|
||||
"motor_y": "new_motor_y",
|
||||
"precision": 2,
|
||||
"step_size_x": 5,
|
||||
"step_size_y": 5,
|
||||
"step_x_y_same": True,
|
||||
"move_with_arrows": True,
|
||||
}
|
||||
}
|
||||
motor_relative_widget.on_config_update(new_config)
|
||||
|
||||
assert motor_relative_widget.motor_x == "new_motor_x"
|
||||
assert motor_relative_widget.motor_y == "new_motor_y"
|
||||
assert motor_relative_widget.precision == 2
|
||||
|
||||
|
||||
def test_move_motor_relative(motor_relative_widget):
|
||||
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
|
||||
# Set step sizes
|
||||
motor_relative_widget.spinBox_step_x.setValue(1)
|
||||
motor_relative_widget.spinBox_step_y.setValue(1)
|
||||
|
||||
# Mock the move_relative method
|
||||
motor_relative_widget.motor_thread.move_relative = MagicMock()
|
||||
|
||||
# Simulate button clicks
|
||||
motor_relative_widget.toolButton_right.click()
|
||||
motor_relative_widget.motor_thread.move_relative.assert_called_with(
|
||||
motor_relative_widget.motor_x, 1
|
||||
)
|
||||
|
||||
motor_relative_widget.toolButton_left.click()
|
||||
motor_relative_widget.motor_thread.move_relative.assert_called_with(
|
||||
motor_relative_widget.motor_x, -1
|
||||
)
|
||||
|
||||
motor_relative_widget.toolButton_up.click()
|
||||
motor_relative_widget.motor_thread.move_relative.assert_called_with(
|
||||
motor_relative_widget.motor_y, 1
|
||||
)
|
||||
|
||||
motor_relative_widget.toolButton_down.click()
|
||||
motor_relative_widget.motor_thread.move_relative.assert_called_with(
|
||||
motor_relative_widget.motor_y, -1
|
||||
)
|
||||
|
||||
|
||||
def test_precision_update(motor_relative_widget):
|
||||
# Capture emitted precision values
|
||||
emitted_values = []
|
||||
|
||||
def capture_precision(precision):
|
||||
emitted_values.append(precision)
|
||||
|
||||
motor_relative_widget.precision_signal.connect(capture_precision)
|
||||
|
||||
# Update precision
|
||||
motor_relative_widget.spinBox_precision.setValue(1)
|
||||
|
||||
assert emitted_values == [1]
|
||||
assert motor_relative_widget.spinBox_step_x.decimals() == 1
|
||||
assert motor_relative_widget.spinBox_step_y.decimals() == 1
|
||||
|
||||
|
||||
def test_sync_step_sizes(motor_relative_widget):
|
||||
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
|
||||
motor_relative_widget.checkBox_same_xy.setChecked(True)
|
||||
|
||||
# Change step size for X
|
||||
motor_relative_widget.spinBox_step_x.setValue(2)
|
||||
|
||||
assert motor_relative_widget.spinBox_step_y.value() == 2
|
||||
|
||||
|
||||
def test_change_motor_relative(motor_relative_widget):
|
||||
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
|
||||
motor_relative_widget.change_motors("aptrx", "aptry")
|
||||
|
||||
assert motor_relative_widget.motor_x == "aptrx"
|
||||
assert motor_relative_widget.motor_y == "aptry"
|
||||
|
||||
|
||||
#######################################################
|
||||
# Motor Control Widgets - MotorCoordinateTable
|
||||
#######################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_coordinate_table(qtbot, mocked_client, motor_thread):
|
||||
widget = MotorCoordinateTable(
|
||||
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
|
||||
)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def test_delete_selected_row(motor_coordinate_table):
|
||||
# Add a coordinate
|
||||
motor_coordinate_table.add_coordinate((1.0, 2.0))
|
||||
motor_coordinate_table.add_coordinate((3.0, 4.0))
|
||||
|
||||
# Select the row
|
||||
motor_coordinate_table.table.selectRow(0)
|
||||
|
||||
# Delete the selected row
|
||||
motor_coordinate_table.delete_selected_row()
|
||||
assert motor_coordinate_table.table.rowCount() == 1
|
||||
|
||||
|
||||
def test_add_coordinate_and_table_update(motor_coordinate_table):
|
||||
# Disable Warning message popups for test
|
||||
motor_coordinate_table.warning_message = False
|
||||
|
||||
# Add coordinate in Individual mode
|
||||
motor_coordinate_table.add_coordinate((1.0, 2.0))
|
||||
assert motor_coordinate_table.table.rowCount() == 1
|
||||
|
||||
# Check if the coordinates match
|
||||
x_item_individual = motor_coordinate_table.table.cellWidget(0, 3) # Assuming X is in column 3
|
||||
y_item_individual = motor_coordinate_table.table.cellWidget(0, 4) # Assuming Y is in column 4
|
||||
assert float(x_item_individual.text()) == 1.0
|
||||
assert float(y_item_individual.text()) == 2.0
|
||||
|
||||
# Switch to Start/Stop and add coordinates
|
||||
motor_coordinate_table.comboBox_mode.setCurrentIndex(1) # Switch mode
|
||||
|
||||
motor_coordinate_table.add_coordinate((3.0, 4.0))
|
||||
motor_coordinate_table.add_coordinate((5.0, 6.0))
|
||||
assert motor_coordinate_table.table.rowCount() == 1
|
||||
|
||||
|
||||
def test_plot_coordinates_signal(motor_coordinate_table):
|
||||
# Connect to the signal
|
||||
def signal_emitted(coordinates, reference_tag, color):
|
||||
nonlocal received
|
||||
received = True
|
||||
assert len(coordinates) == 1 # Assuming one coordinate was added
|
||||
assert reference_tag in ["Individual", "Start", "Stop"]
|
||||
assert color in ["green", "blue", "red"]
|
||||
|
||||
received = False
|
||||
motor_coordinate_table.plot_coordinates_signal.connect(signal_emitted)
|
||||
|
||||
# Add a coordinate and check signal
|
||||
motor_coordinate_table.add_coordinate((1.0, 2.0))
|
||||
assert received
|
||||
|
||||
|
||||
def test_move_motor_action(motor_coordinate_table):
|
||||
# Add a coordinate
|
||||
motor_coordinate_table.add_coordinate((1.0, 2.0))
|
||||
|
||||
# Mock the motor thread move_absolute function
|
||||
motor_coordinate_table.motor_thread.move_absolute = MagicMock()
|
||||
|
||||
# Trigger the move action
|
||||
move_button = motor_coordinate_table.table.cellWidget(0, 1)
|
||||
move_button.click()
|
||||
|
||||
motor_coordinate_table.motor_thread.move_absolute.assert_called_with(
|
||||
motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0)
|
||||
)
|
||||
|
||||
|
||||
def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
|
||||
motor_coordinate_table.warning_message = False
|
||||
motor_coordinate_table.set_precision(3)
|
||||
motor_coordinate_table.comboBox_mode.setCurrentIndex(0)
|
||||
|
||||
# This list will store the signals emitted during the test
|
||||
emitted_signals = []
|
||||
|
||||
def signal_emitted(coordinates, reference_tag, color):
|
||||
emitted_signals.append((coordinates, reference_tag, color))
|
||||
|
||||
motor_coordinate_table.plot_coordinates_signal.connect(signal_emitted)
|
||||
|
||||
# Add new coordinates
|
||||
motor_coordinate_table.add_coordinate((1.0, 2.0))
|
||||
qtbot.wait(100)
|
||||
|
||||
# Verify the signals
|
||||
assert len(emitted_signals) > 0, "No signals were emitted."
|
||||
|
||||
for coordinates, reference_tag, color in emitted_signals:
|
||||
assert len(coordinates) > 0, "Coordinates list is empty."
|
||||
assert reference_tag == "Individual"
|
||||
assert color == "green"
|
||||
assert motor_coordinate_table.table.cellWidget(0, 3).text() == "1.000"
|
||||
assert motor_coordinate_table.table.cellWidget(0, 4).text() == "2.000"
|
||||
|
||||
|
||||
#######################################################
|
||||
# MotorControl examples compilations
|
||||
#######################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_app(qtbot, mocked_client):
|
||||
widget = MotorControlApp(config=CONFIG_DEFAULT, client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_motor_app_initialization(motor_app):
|
||||
assert isinstance(motor_app, MotorControlApp)
|
||||
assert motor_app.client is not None
|
||||
assert motor_app.config == CONFIG_DEFAULT
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_control_map(qtbot, mocked_client):
|
||||
widget = MotorControlMap(config=CONFIG_DEFAULT, client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_motor_control_map_initialization(motor_control_map):
|
||||
assert isinstance(motor_control_map, MotorControlMap)
|
||||
assert motor_control_map.client is not None
|
||||
assert motor_control_map.config == CONFIG_DEFAULT
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_control_panel(qtbot, mocked_client):
|
||||
widget = MotorControlPanel(config=CONFIG_DEFAULT, client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_motor_control_panel_initialization(motor_control_panel):
|
||||
assert isinstance(motor_control_panel, MotorControlPanel)
|
||||
assert motor_control_panel.client is not None
|
||||
assert motor_control_panel.config == CONFIG_DEFAULT
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_control_panel_absolute(qtbot, mocked_client):
|
||||
widget = MotorControlPanelAbsolute(config=CONFIG_DEFAULT, client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_motor_control_panel_absolute_initialization(motor_control_panel_absolute):
|
||||
assert isinstance(motor_control_panel_absolute, MotorControlPanelAbsolute)
|
||||
assert motor_control_panel_absolute.client is not None
|
||||
assert motor_control_panel_absolute.config == CONFIG_DEFAULT
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def motor_control_panel_relative(qtbot, mocked_client):
|
||||
widget = MotorControlPanelRelative(config=CONFIG_DEFAULT, client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_motor_control_panel_relative_initialization(motor_control_panel_relative):
|
||||
assert isinstance(motor_control_panel_relative, MotorControlPanelRelative)
|
||||
assert motor_control_panel_relative.client is not None
|
||||
assert motor_control_panel_relative.config == CONFIG_DEFAULT
|
||||
0
tests/test_msgs/__init__.py
Normal file
0
tests/test_msgs/__init__.py
Normal file
989
tests/test_msgs/available_scans_message.py
Normal file
989
tests/test_msgs/available_scans_message.py
Normal file
@@ -0,0 +1,989 @@
|
||||
from bec_lib.messages import AvailableResourceMessage
|
||||
|
||||
available_scans_message = AvailableResourceMessage(
|
||||
resource={
|
||||
"acquire": {
|
||||
"class": "Acquire",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A simple acquisition at the current position.\n\n Args:\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.acquire(exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"interactive_scan_trigger": {
|
||||
"class": "AddInteractiveScanPoint",
|
||||
"base_class": "ScanComponent",
|
||||
"arg_input": {"device": "device"},
|
||||
"required_kwargs": ["required"],
|
||||
"arg_bundle_size": {"bundle": 1, "min": 1, "max": None},
|
||||
"scan_report_hint": "",
|
||||
"doc": "\n An interactive scan for one or more motors.\n\n Args:\n *args: devices\n exp_time: exposure time in s\n steps: number of steps (please note: 5 steps == 6 positions)\n relative: Start from an absolute or relative position\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.interactive_scan_trigger()\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"close_interactive_scan": {
|
||||
"class": "CloseInteractiveScan",
|
||||
"base_class": "ScanComponent",
|
||||
"arg_input": {},
|
||||
"required_kwargs": ["required"],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "",
|
||||
"doc": "\n An interactive scan for one or more motors.\n\n Args:\n *args: devices\n exp_time: exposure time in s\n steps: number of steps (please note: 5 steps == 6 positions)\n relative: Start from an absolute or relative position\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.close_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"close_scan_def": {
|
||||
"class": "CloseScanDef",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": 0, "max": 0},
|
||||
"scan_report_hint": "table",
|
||||
"doc": None,
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "device_manager",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceManagerBase",
|
||||
},
|
||||
{
|
||||
"name": "monitored",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "list",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{"name": "metadata", "kind": "KEYWORD_ONLY", "default": None, "annotation": "dict"},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"close_scan_group": {
|
||||
"class": "CloseScanGroup",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": 0, "max": 0},
|
||||
"scan_report_hint": None,
|
||||
"doc": None,
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "device_manager",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceManagerBase",
|
||||
},
|
||||
{
|
||||
"name": "monitored",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "list",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{"name": "metadata", "kind": "KEYWORD_ONLY", "default": None, "annotation": "dict"},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"cont_line_scan": {
|
||||
"class": "ContLineScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "start": "float", "stop": "float"},
|
||||
"required_kwargs": ["steps", "relative"],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A line scan for one or more motors.\n\n Args:\n *args (Device, float, float): pairs of device / start position / end position\n exp_time (float): exposure time in seconds. Default is 0.\n steps (int): number of steps. Default is 10.\n relative (bool): if True, the motors will be moved relative to their current position. Default is False.\n burst_at_each_point (int): number of exposures at each point. Default is 1.\n offset (float): offset in motor units. Default is 100.\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.cont_line_scan(dev.motor1, -5, 5, steps=10, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{"name": "steps", "kind": "KEYWORD_ONLY", "default": 10, "annotation": "int"},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{"name": "offset", "kind": "KEYWORD_ONLY", "default": 100, "annotation": "float"},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"device_rpc": {
|
||||
"class": "DeviceRPC",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": ["device", "str", "list", "dict"],
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 4, "min": 1, "max": 1},
|
||||
"scan_report_hint": None,
|
||||
"doc": None,
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "device_manager",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceManagerBase",
|
||||
},
|
||||
{
|
||||
"name": "monitored",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "list",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{"name": "metadata", "kind": "KEYWORD_ONLY", "default": None, "annotation": "dict"},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"fermat_scan": {
|
||||
"class": "FermatSpiralScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "start": "float", "stop": "float"},
|
||||
"required_kwargs": ["step", "relative"],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 2, "max": 2},
|
||||
"scan_report_hint": "table",
|
||||
"doc": '\n A scan following Fermat\'s spiral.\n\n Args:\n *args: pairs of device / start position / end position arguments\n step (float): step size in motor units. Default is 0.1.\n exp_time (float): exposure time in seconds. Default is 0.\n settling_time (float): settling time in seconds. Default is 0.\n relative (bool): if True, the motors will be moved relative to their current position. Default is False.\n burst_at_each_point (int): number of exposures at each point. Default is 1.\n spiral_type (float): type of spiral to use. Default is 0.\n optim_trajectory (str): trajectory optimization method. Default is None. Options are "corridor" and "none".\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.fermat_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, step=0.5, exp_time=0.1, relative=True, optim_trajectory="corridor")\n\n ',
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "step", "kind": "KEYWORD_ONLY", "default": 0.1, "annotation": "float"},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{
|
||||
"name": "settling_time",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 0,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "spiral_type",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 0,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "optim_trajectory",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": {"Literal": ["corridor", None]},
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"line_scan": {
|
||||
"class": "LineScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "start": "float", "stop": "float"},
|
||||
"required_kwargs": ["steps", "relative"],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 1, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A line scan for one or more motors.\n\n Args:\n *args (Device, float, float): pairs of device / start position / end position\n exp_time (float): exposure time in s. Default: 0\n steps (int): number of steps. Default: 10\n relative (bool): if True, the start and end positions are relative to the current position. Default: False\n burst_at_each_point (int): number of acquisition per point. Default: 1\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.line_scan(dev.motor1, -5, 5, dev.motor2, -5, 5, steps=10, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{"name": "steps", "kind": "KEYWORD_ONLY", "default": None, "annotation": "int"},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"list_scan": {
|
||||
"class": "ListScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "positions": "list"},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 2, "min": 1, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A scan following the positions specified in a list.\n Please note that all lists must be of equal length.\n\n Args:\n *args: pairs of motors and position lists\n relative: Start from an absolute or relative position\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.list_scan(dev.motor1, [0,1,2,3,4], dev.motor2, [4,3,2,1,0], exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"monitor_scan": {
|
||||
"class": "MonitorScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "start": "float", "stop": "float"},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n Readout all primary devices at each update of the monitored device.\n\n Args:\n device (Device): monitored device\n start (float): start position of the monitored device\n stop (float): stop position of the monitored device\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.monitor_scan(dev.motor1, -5, 5, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "device",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "stop",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"mv": {
|
||||
"class": "Move",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {"device": "device", "target": "float"},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 2, "min": 1, "max": None},
|
||||
"scan_report_hint": None,
|
||||
"doc": "\n Move device(s) to an absolute position\n Args:\n *args (Device, float): pairs of device / position arguments\n relative (bool): if True, move relative to current position\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.mv(dev.samx, 1, dev.samy,2)\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"open_interactive_scan": {
|
||||
"class": "OpenInteractiveScan",
|
||||
"base_class": "ScanComponent",
|
||||
"arg_input": {"device": "device"},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 1, "min": 1, "max": None},
|
||||
"scan_report_hint": "",
|
||||
"doc": "\n An interactive scan for one or more motors.\n\n Args:\n *args: devices\n exp_time: exposure time in s\n steps: number of steps (please note: 5 steps == 6 positions)\n relative: Start from an absolute or relative position\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.open_interactive_scan(dev.motor1, dev.motor2, exp_time=0.1)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"open_scan_def": {
|
||||
"class": "OpenScanDef",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": 0, "max": 0},
|
||||
"scan_report_hint": None,
|
||||
"doc": None,
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "device_manager",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceManagerBase",
|
||||
},
|
||||
{
|
||||
"name": "monitored",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "list",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{"name": "metadata", "kind": "KEYWORD_ONLY", "default": None, "annotation": "dict"},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"round_roi_scan": {
|
||||
"class": "RoundROIScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {
|
||||
"motor_1": "device",
|
||||
"motor_2": "device",
|
||||
"width_1": "float",
|
||||
"width_2": "float",
|
||||
},
|
||||
"required_kwargs": ["dr", "nth", "relative"],
|
||||
"arg_bundle_size": {"bundle": 4, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A scan following a round-roi-like pattern.\n\n Args:\n *args: motor1, width for motor1, motor2, width for motor2,\n dr (float): shell width. Default is 1.\n nth (int): number of points in the first shell. Default is 5.\n exp_time (float): exposure time in seconds. Default is 0.\n relative (bool): Start from an absolute or relative position. Default is False.\n burst_at_each_point (int): number of acquisition per point. Default is 1.\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.round_roi_scan(dev.motor1, 20, dev.motor2, 20, dr=2, nth=3, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "dr", "kind": "KEYWORD_ONLY", "default": 1, "annotation": "float"},
|
||||
{"name": "nth", "kind": "KEYWORD_ONLY", "default": 5, "annotation": "int"},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"round_scan": {
|
||||
"class": "RoundScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {
|
||||
"motor_1": "device",
|
||||
"motor_2": "device",
|
||||
"inner_ring": "float",
|
||||
"outer_ring": "float",
|
||||
"number_of_rings": "int",
|
||||
"number_of_positions_in_first_ring": "int",
|
||||
},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 6, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A scan following a round shell-like pattern.\n\n Args:\n *args: motor1, motor2, inner ring, outer ring, number of rings, number of positions in the first ring\n relative (bool): if True, the motors will be moved relative to their current position. Default is False.\n burst_at_each_point (int): number of exposures at each point. Default is 1.\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.round_scan(dev.motor1, dev.motor2, 0, 25, 5, 3, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"round_scan_fly": {
|
||||
"class": "RoundScanFlySim",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {
|
||||
"flyer": "device",
|
||||
"inner_ring": "float",
|
||||
"outer_ring": "float",
|
||||
"number_of_rings": "int",
|
||||
"number_of_positions_in_first_ring": "int",
|
||||
},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 5, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A fly scan following a round shell-like pattern.\n\n Args:\n *args: motor1, motor2, inner ring, outer ring, number of rings, number of positions in the first ring\n relative: Start from an absolute or relative position\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.round_scan_fly(dev.flyer_sim, 0, 50, 5, 3, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"grid_scan": {
|
||||
"class": "Scan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {"device": "device", "start": "float", "stop": "float", "steps": "int"},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 4, "min": 2, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n Scan two motors in a grid.\n\n Args:\n *args (Device, float, float, int): pairs of device / start / stop / steps arguments\n exp_time (float): exposure time in seconds. Default is 0.\n settling_time (float): settling time in seconds. Default is 0.\n relative (bool): if True, the motors will be moved relative to their current position. Default is False.\n burst_at_each_point (int): number of exposures at each point. Default is 1.\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.grid_scan(dev.motor1, -5, 5, 10, dev.motor2, -5, 5, 10, exp_time=0.1, relative=True)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0, "annotation": "float"},
|
||||
{
|
||||
"name": "settling_time",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 0,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "bool",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"time_scan": {
|
||||
"class": "TimeScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": ["points", "interval"],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": '\n Trigger and readout devices at a fixed interval.\n Note that the interval time cannot be less than the exposure time.\n The effective "sleep" time between points is\n sleep_time = interval - exp_time\n\n Args:\n points: number of points\n interval: time interval between points\n exp_time: exposure time in s\n burst: number of acquisition per point\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.time_scan(points=10, interval=1.5, exp_time=0.1, relative=True)\n\n ',
|
||||
"signature": [
|
||||
{
|
||||
"name": "points",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "interval",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "exp_time",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": 0,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "burst_at_each_point",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": 1,
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"umv": {
|
||||
"class": "UpdatedMove",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {"device": "device", "target": "float"},
|
||||
"required_kwargs": ["relative"],
|
||||
"arg_bundle_size": {"bundle": 2, "min": 1, "max": None},
|
||||
"scan_report_hint": "readback",
|
||||
"doc": "\n Move device(s) to an absolute position and show live updates. This is a blocking call. For non-blocking use Move.\n Args:\n *args (Device, float): pairs of device / position arguments\n relative (bool): if True, move relative to current position\n\n Returns:\n ScanReport\n\n Examples:\n >>> scans.umv(dev.samx, 1, dev.samy,2)\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "relative",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": False,
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"lamni_fermat_scan": {
|
||||
"class": "LamNIFermatScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": ["fov_size", "exp_time", "step", "angle"],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A LamNI scan following Fermat's spiral.\n\n Kwargs:\n fov_size [um]: Fov in the piezo plane (i.e. piezo range). Max 80 um\n step [um]: stepsize\n shift_x/y [mm]: extra shift in x/y. The shift is directly applied to the scan. It will not be auto rotated. (default 0).\n center_x/center_y [mm]: center position in x/y at 0 deg. This shift is rotated\n using the geometry of LamNI\n It is determined by the first 'click' in the x-ray eye alignemnt procedure\n angle [deg]: rotation angle (will rotate first)\n scan_type: fly (i.e. HW triggered step in case of LamNI) or step\n stitch_x/y: shift scan to adjacent stitch region\n fov_circular [um]: generate a circular field of view in the sample plane. This is an additional cropping to fov_size.\n stitch_overlap [um]: overlap of the stitched regions\n Returns:\n\n Examples:\n >>> scans.lamni_fermat_scan(fov_size=[20], step=0.5, exp_time=0.1)\n >>> scans.lamni_fermat_scan(fov_size=[20, 25], center_x=0.02, center_y=0, shift_x=0, shift_y=0, angle=0, step=0.5, fov_circular=0, exp_time=0.1)\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"lamni_move_to_scan_center": {
|
||||
"class": "LamNIMoveToScanCenter",
|
||||
"base_class": "RequestBase",
|
||||
"arg_input": {"shift_x": "float", "shift_y": "float", "angle": "float"},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 1, "max": 1},
|
||||
"scan_report_hint": None,
|
||||
"doc": "\n Move LamNI to a new scan center.\n\n Args:\n *args: shift x, shift y, tomo angle in deg\n\n Examples:\n >>> scans.lamni_move_to_scan_center(1.2, 2.8, 12.5)\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"owis_grid": {
|
||||
"class": "OwisGrid",
|
||||
"base_class": "FlyScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "scan_progress",
|
||||
"doc": "Owis-based grid scan.",
|
||||
"signature": [
|
||||
{
|
||||
"name": "start_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "end_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "interval_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "start_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "end_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "interval_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0.1, "annotation": "float"},
|
||||
{
|
||||
"name": "readout_time",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 0.003,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"sgalil_grid": {
|
||||
"class": "SgalilGrid",
|
||||
"base_class": "FlyScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "scan_progress",
|
||||
"doc": "\n SGalil-based grid scan.\n\n Args:\n start_y (float): start position of y axis (fast axis)\n end_y (float): end position of y axis (fast axis)\n interval_y (int): number of points in y axis\n start_x (float): start position of x axis (slow axis)\n end_x (float): end position of x axis (slow axis)\n interval_x (int): number of points in x axis\n exp_time (float): exposure time in seconds. Default is 0.1s\n readout_time (float): readout time in seconds, minimum of 3e-3s (3ms)\n\n Exp:\n scans.sgalil_grid(start_y = val1, end_y= val1, interval_y = val1, start_x = val1, end_x = val1, interval_x = val1, exp_time = 0.02, readout_time = 3e-3)\n\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "start_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "end_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "interval_y",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "start_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "end_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "interval_x",
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "int",
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{"name": "exp_time", "kind": "KEYWORD_ONLY", "default": 0.1, "annotation": "float"},
|
||||
{
|
||||
"name": "readout_time",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": 0.1,
|
||||
"annotation": "float",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"hyst_scan": {
|
||||
"class": "HystScan",
|
||||
"base_class": "ScanBase",
|
||||
"arg_input": {
|
||||
"field_motor": "device",
|
||||
"start_field": "float",
|
||||
"end_field": "float",
|
||||
"mono": "device",
|
||||
"energy1": "float",
|
||||
"energy2": "float",
|
||||
},
|
||||
"required_kwargs": [],
|
||||
"arg_bundle_size": {"bundle": 3, "min": 1, "max": 1},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "\n A hysteresis scan.\n\n scans.hyst_scan(field_motor, start_field, end_field, mono, energy1, energy2)\n\n Examples:\n >>> scans.hyst_scan(dev.field_x, 0, 0.5, dev.mono, 600, 640, ramp_rate=2)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
"otf_scan": {
|
||||
"class": "OTFScan",
|
||||
"base_class": "FlyScanBase",
|
||||
"arg_input": {},
|
||||
"required_kwargs": ["e1", "e2", "time"],
|
||||
"arg_bundle_size": {"bundle": 0, "min": None, "max": None},
|
||||
"scan_report_hint": "table",
|
||||
"doc": "Scans the energy from e1 to e2 in <time> minutes.\n\n Examples:\n >>> scans.otf_scan(e1=700, e2=740, time=4)\n\n ",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "parameter",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
62
tests/test_plot_base.py
Normal file
62
tests/test_plot_base.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
import pytest
|
||||
from .client_mocks import mocked_client
|
||||
from .test_bec_figure import bec_figure
|
||||
|
||||
|
||||
def test_init_plot_base(bec_figure):
|
||||
plot_base = bec_figure.add_widget(widget_type="PlotBase", widget_id="test_plot")
|
||||
assert plot_base is not None
|
||||
assert plot_base.config.widget_class == "BECPlotBase"
|
||||
assert plot_base.config.gui_id == "test_plot"
|
||||
|
||||
|
||||
def test_plot_base_axes_by_separate_methods(bec_figure):
|
||||
plot_base = bec_figure.add_widget(widget_type="PlotBase", 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.titleLabel.text == "Test Title"
|
||||
assert plot_base.config.axis.title == "Test Title"
|
||||
assert plot_base.getAxis("bottom").labelText == "Test x Label"
|
||||
assert plot_base.config.axis.x_label == "Test x Label"
|
||||
assert plot_base.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.ctrl.xGridCheck.isChecked() == True
|
||||
assert plot_base.ctrl.yGridCheck.isChecked() == True
|
||||
assert plot_base.ctrl.logXCheck.isChecked() == True
|
||||
assert plot_base.ctrl.logYCheck.isChecked() == True
|
||||
|
||||
|
||||
def test_plot_base_axes_added_by_kwargs(bec_figure):
|
||||
plot_base = bec_figure.add_widget(widget_type="PlotBase", 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.titleLabel.text == "Test Title"
|
||||
assert plot_base.config.axis.title == "Test Title"
|
||||
assert plot_base.getAxis("bottom").labelText == "Test x Label"
|
||||
assert plot_base.config.axis.x_label == "Test x Label"
|
||||
assert plot_base.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.ctrl.logXCheck.isChecked() == True
|
||||
assert plot_base.ctrl.logYCheck.isChecked() == True
|
||||
26
tests/test_problem.py
Normal file
26
tests/test_problem.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.cli.client import BECFigure
|
||||
from bec_widgets.cli.server import BECWidgetsCLIServer
|
||||
from bec_widgets.utils.bec_dispatcher import _BECDispatcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_server(qtbot):
|
||||
# make a new dispatcher (not using the singleton), since the server is supposed to run in another process
|
||||
dispatcher = _BECDispatcher()
|
||||
server = BECWidgetsCLIServer(gui_id="id_test", dispatcher=dispatcher)
|
||||
qtbot.addWidget(server.fig)
|
||||
qtbot.waitExposed(server.fig)
|
||||
qtbot.wait(200)
|
||||
yield server
|
||||
server.client.shutdown()
|
||||
|
||||
|
||||
def test_rpc_waveform1d(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
ax = fig.add_plot()
|
||||
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
|
||||
curve.set_color("red")
|
||||
curve = ax.curves[0]
|
||||
curve.set_color("blue")
|
||||
@@ -1,25 +1,15 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import os
|
||||
import pickle
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import msgpack
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QLineEdit
|
||||
|
||||
from bec_widgets.widgets import ScanControl
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
# TODO there has to be a better way to mock messages than this, in this case I just took the msg from bec
|
||||
def load_test_msg(msg_name):
|
||||
"""Helper function to load msg from pickle file."""
|
||||
msg_path = os.path.join(os.path.dirname(__file__), "test_msgs", f"{msg_name}.pkl")
|
||||
with open(msg_path, "rb") as f:
|
||||
msg = pickle.load(f)
|
||||
return msg
|
||||
|
||||
|
||||
packed_message = load_test_msg("msg_dict")["available_scans"]
|
||||
from .test_msgs.available_scans_message import available_scans_message
|
||||
|
||||
|
||||
class FakePositioner:
|
||||
@@ -45,7 +35,7 @@ def mocked_client():
|
||||
client = MagicMock()
|
||||
|
||||
# Mock the producer.get method to return the packed message
|
||||
client.producer.get.return_value = packed_message
|
||||
client.producer.get.return_value = available_scans_message
|
||||
|
||||
# # Mock the device_manager.devices attribute to return a mock object for samx
|
||||
client.device_manager.devices = MagicMock()
|
||||
@@ -66,7 +56,7 @@ def scan_control(qtbot, mocked_client): # , mock_dev):
|
||||
|
||||
def test_populate_scans(scan_control, mocked_client):
|
||||
# The comboBox should be populated with all scan from the message right after initialization
|
||||
expected_scans = msgpack.loads(packed_message).keys()
|
||||
expected_scans = available_scans_message.resource.keys()
|
||||
assert scan_control.comboBox_scan_selection.count() == len(expected_scans)
|
||||
for scan in expected_scans: # Each scan should be in the comboBox
|
||||
assert scan_control.comboBox_scan_selection.findText(scan) != -1
|
||||
@@ -77,7 +67,7 @@ def test_populate_scans(scan_control, mocked_client):
|
||||
) # TODO now only for line_scan and grid_scan, later for all loaded scans
|
||||
def test_on_scan_selected(scan_control, scan_name):
|
||||
# Expected scan info from the message signature
|
||||
expected_scan_info = msgpack.loads(packed_message)[scan_name]
|
||||
expected_scan_info = available_scans_message.resource[scan_name]
|
||||
|
||||
# Select a scan from the comboBox
|
||||
scan_control.comboBox_scan_selection.setCurrentText(scan_name)
|
||||
@@ -110,7 +100,7 @@ def test_on_scan_selected(scan_control, scan_name):
|
||||
@pytest.mark.parametrize("scan_name", ["line_scan", "grid_scan"])
|
||||
def test_add_remove_bundle(scan_control, scan_name):
|
||||
# Expected scan info from the message signature
|
||||
expected_scan_info = msgpack.loads(packed_message)[scan_name]
|
||||
expected_scan_info = available_scans_message.resource[scan_name]
|
||||
|
||||
# Select a scan from the comboBox
|
||||
scan_control.comboBox_scan_selection.setCurrentText(scan_name)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from bec_widgets.widgets.scan_plot import scan_plot
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=missing-function-docstring
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from bec_widgets.validation.monitor_config_validator import (
|
||||
@@ -8,7 +8,7 @@ from bec_widgets.validation.monitor_config_validator import (
|
||||
PlotConfig,
|
||||
)
|
||||
|
||||
from test_bec_monitor import mocked_client
|
||||
from .test_bec_monitor import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
||||
411
tests/test_waveform1d.py
Normal file
411
tests/test_waveform1d.py
Normal file
@@ -0,0 +1,411 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.plots.waveform1d import SignalData, Signal, CurveConfig
|
||||
from .client_mocks import mocked_client
|
||||
from .test_bec_figure import bec_figure
|
||||
|
||||
|
||||
def test_adding_curve_to_waveform(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
# adding curve which is in bec - only names
|
||||
c1 = w1.add_curve_scan(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_scan(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_scan(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_scan(
|
||||
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_scan(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(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i", gui_id="test_curve")
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
w1.add_curve_scan(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(bec_figure):
|
||||
w1_config_input = {
|
||||
"widget_class": "BECWaveform1D",
|
||||
"gui_id": "widget_1",
|
||||
"parent_id": "BECFigure_1708689320.788527",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"axis": {
|
||||
"title": "Widget 1",
|
||||
"x_label": None,
|
||||
"y_label": None,
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_lim": (1, 10),
|
||||
"y_lim": None,
|
||||
"x_grid": False,
|
||||
"y_grid": False,
|
||||
},
|
||||
"color_palette": "plasma",
|
||||
"curves": {
|
||||
"bpm4i-bpm4i": {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.226847",
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None},
|
||||
},
|
||||
},
|
||||
"curve-custom": {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.22867",
|
||||
"parent_id": "widget_1",
|
||||
"label": "curve-custom",
|
||||
"color": "blue",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"pen_style": "dashdot",
|
||||
"source": "custom",
|
||||
"signals": None,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform", config=w1_config_input)
|
||||
|
||||
w1_config_output = w1.get_config()
|
||||
|
||||
assert w1_config_input == w1_config_output
|
||||
assert w1.titleLabel.text == "Widget 1"
|
||||
assert w1.config.axis.title == "Widget 1"
|
||||
|
||||
|
||||
def test_change_gui_id(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
c1 = w1.add_curve_scan(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(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
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",
|
||||
label="bpm4i-bpm4i",
|
||||
color="#cc4778",
|
||||
symbol="o",
|
||||
symbol_color=None,
|
||||
symbol_size=5,
|
||||
pen_width=2,
|
||||
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
|
||||
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
|
||||
assert c1.get_config() == c1_expected_config.model_dump()
|
||||
|
||||
|
||||
def test_getting_curve_errors(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
c1 = w1.add_curve_scan(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(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
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 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.add_curve_scan(x_name="samx", y_name="bpm4i")
|
||||
w1.add_curve_scan(x_name="samx", y_name="bpm3a")
|
||||
w1.remove_curve(0)
|
||||
w1.remove_curve("bpm3a-bpm3a")
|
||||
|
||||
assert len(w1.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(bec_figure, qtbot):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
c1 = w1.add_curve_scan(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() == {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None},
|
||||
}
|
||||
|
||||
|
||||
def test_change_curve_appearance_args(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
c1 = w1.add_curve_scan(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() == {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None},
|
||||
}
|
||||
|
||||
|
||||
def test_set_custom_curve_data(bec_figure, qtbot):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
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_get_all_data(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
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(bec_figure):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
c1_config_input = {
|
||||
"widget_class": "BECCurve",
|
||||
"gui_id": "BECCurve_1708689321.226847",
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": 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(bec_figure, qtbot):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform")
|
||||
|
||||
c1 = w1.add_curve_scan(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}},
|
||||
},
|
||||
"scanID": 1,
|
||||
}
|
||||
# Mock scan_storage.find_scan_by_ID
|
||||
mock_scan_data_waveform = MagicMock()
|
||||
mock_scan_data_waveform.data = {
|
||||
device_name: {
|
||||
entry: 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(bec_figure, qtbot):
|
||||
w1 = bec_figure.add_plot(widget_id="test_waveform_history_val")
|
||||
|
||||
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
|
||||
|
||||
mock_scan_data = {
|
||||
"samx": {"samx": MagicMock(val=np.array([1, 2, 3]))}, # Use MagicMock for .val
|
||||
"bpm4i": {"bpm4i": MagicMock(val=np.array([4, 5, 6]))}, # Use MagicMock for .val
|
||||
}
|
||||
|
||||
mock_scan_storage = MagicMock()
|
||||
mock_scan_storage.find_scan_by_ID.return_value = MagicMock(data=mock_scan_data)
|
||||
w1.queue.scan_storage = mock_scan_storage
|
||||
|
||||
fake_scanID = "fake_scanID"
|
||||
w1.scan_history(scanID=fake_scanID)
|
||||
|
||||
qtbot.wait(500)
|
||||
|
||||
x_data, y_data = c1.get_data()
|
||||
|
||||
assert np.array_equal(x_data, [1, 2, 3])
|
||||
assert np.array_equal(y_data, [4, 5, 6])
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import pytest
|
||||
from qtpy.QtWidgets import (
|
||||
QWidget,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
Reference in New Issue
Block a user