1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-16 21:45:35 +02:00

Compare commits

...

39 Commits

Author SHA1 Message Date
7cdc99fba0 feat(general_gui): dark/light theme switcher 2024-06-26 21:33:15 +02:00
2a24a64f82 feat(general_gui): set icons; links moved to separate script 2024-06-26 21:29:43 +02:00
fb043f86d1 feat(general_gui): added icon 2024-06-26 21:29:43 +02:00
f7e46e5d8b fix(qdarktheme): fixed theme for dark 2024-06-26 21:29:43 +02:00
b37ed0d45c fix(ui_loader): generalized ui loader 2024-06-26 21:29:43 +02:00
bfb3da0104 wip 2024-06-26 21:29:43 +02:00
6623aa83df feat(jupyter_console): plugin added for QtDesigner 2024-06-26 21:29:43 +02:00
2c816345b5 fix(general_app): adapted to new custom loader 2024-06-26 21:29:43 +02:00
4a36f4f717 feat(utils):ui custom loader moved to utils and integrated into UILoader 2024-06-26 21:29:43 +02:00
2693ce4c38 fix(motor_movement): removed init to avoid circular imports 2024-06-26 21:29:43 +02:00
acb50febf1 feat(general_app): general app launcher added 2024-06-26 21:29:43 +02:00
dc9c1ec099 feat(scan_control): plugin added for QtDesigner 2024-06-26 21:29:43 +02:00
167a1e8f00 WIP - general app creation 2024-06-26 21:29:43 +02:00
0517d9f3e9 WIP 2024-06-26 21:29:43 +02:00
93a410bbf6 feat(vscode): plugin added for QtDesigner 2024-06-26 21:29:43 +02:00
30aacadbf0 feat(bec_status_box): plugin added for QtDesigner 2024-06-26 21:29:43 +02:00
3faee98ec8 feat(widgets): added simple bec queue widget 2024-06-26 20:42:37 +02:00
ca02132c8d refactor(dispatcher): cleanup 2024-06-26 20:42:37 +02:00
semantic-release
cb4ef25b73 0.74.1
Automatically generated by python-semantic-release
2024-06-26 13:10:06 +00:00
c8b7367815 fix(rings): rings properties updated right after setting 2024-06-26 11:46:21 +02:00
a268caaa30 test(bec_figure): tests for removing widgets with rpc e2e 2024-06-26 11:41:29 +02:00
6b25abff70 fix(motor_map): motor map can be removed from BECFigure with .remove() 2024-06-26 11:24:46 +02:00
21c807f358 chore: sorted dependencies alphabetically 2024-06-26 10:27:46 +02:00
56fdae4275 build: added missing pytest-bec-e2e dependency; closes #219 2024-06-26 10:24:25 +02:00
e6a06c9f43 build: fixed dependency ranges; closes #135 2024-06-26 10:09:48 +02:00
f979a63d3d docs: fixed doc string 2024-06-26 09:46:14 +02:00
semantic-release
327bc54e22 0.74.0
Automatically generated by python-semantic-release
2024-06-25 16:44:56 +00:00
a51b15da3f docs(becfigure): docs added 2024-06-25 18:37:23 +02:00
7271b422f9 test(waveform1d): dap e2e test added 2024-06-25 18:37:23 +02:00
1866ba66c8 feat(waveform1d): dap LMFit model can be added to plot 2024-06-25 18:37:20 +02:00
semantic-release
6175a04a90 0.73.2
Automatically generated by python-semantic-release
2024-06-25 16:24:11 +00:00
7120f3e93b fix(vscode): only run terminate if the process is still alive 2024-06-25 18:17:02 +02:00
acc13183e2 fix(rpc): trigger shutdown of server when gui is terminated 2024-06-25 16:45:39 +02:00
f75fc19c5b fix(rpc): remove of calling "close" and waiting for gui_is_alive 2024-06-25 15:22:29 +02:00
semantic-release
2650c8b8cf 0.73.1
Automatically generated by python-semantic-release
2024-06-25 10:25:13 +00:00
1de3cbf65a fix(ringprogressbar): removed hard-coded endpoint strings 2024-06-25 12:18:14 +02:00
semantic-release
4a9d0c9e44 0.73.0
Automatically generated by python-semantic-release
2024-06-25 10:05:53 +00:00
88ecd05b95 test: add test for imageitem 2024-06-25 11:58:55 +02:00
df812eaad5 feat: add new default scaling of image_item 2024-06-25 11:58:55 +02:00
58 changed files with 1828 additions and 190 deletions

View File

@@ -1,5 +1,71 @@
# CHANGELOG
## v0.74.1 (2024-06-26)
### Build
* build: added missing pytest-bec-e2e dependency; closes #219 ([`56fdae4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56fdae42757bdb9fa301c1e425a77e98b6eaf92b))
* build: fixed dependency ranges; closes #135 ([`e6a06c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6a06c9f43e0ad6bbfcfa550a2f580d2a27aff66))
### Chore
* chore: sorted dependencies alphabetically ([`21c807f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/21c807f35831fdd1ef2e488ab90edae4719f0cb7))
### Documentation
* docs: fixed doc string ([`f979a63`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f979a63d3d1a008f80e500510909750878ff4303))
### Fix
* fix(rings): rings properties updated right after setting ([`c8b7367`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c8b7367815b095f8e4aa8b819481efb701f2e542))
* fix(motor_map): motor map can be removed from BECFigure with .remove() ([`6b25abf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b25abff70280271e2eeb70450553c05d4b7c99c))
### Test
* test(bec_figure): tests for removing widgets with rpc e2e ([`a268caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a268caaa30711fcc7ece542d24578d74cbf65c77))
## v0.74.0 (2024-06-25)
### Documentation
* docs(becfigure): docs added ([`a51b15d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a51b15da3f5e83e0c897a0342bdb05b9c677a179))
### Feature
* feat(waveform1d): dap LMFit model can be added to plot ([`1866ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1866ba66c8e3526661beb13fff3e13af6a0ae562))
### Test
* test(waveform1d): dap e2e test added ([`7271b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7271b422f98ef9264970d708811c414b69a644db))
## v0.73.2 (2024-06-25)
### Fix
* fix(vscode): only run terminate if the process is still alive ([`7120f3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7120f3e93b054b788f15e2d5bcd688e3c140c1ce))
* fix(rpc): trigger shutdown of server when gui is terminated ([`acc1318`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/acc13183e28030e3ca9af21bb081e1eed081622b))
* fix(rpc): remove of calling "close" and waiting for gui_is_alive ([`f75fc19`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f75fc19c5b10022763252917ca473f404a25165a))
## v0.73.1 (2024-06-25)
### Fix
* fix(ringprogressbar): removed hard-coded endpoint strings ([`1de3cbf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1de3cbf65a1832150917a7549a1bf3efdee6371a))
## v0.73.0 (2024-06-25)
### Feature
* feat: add new default scaling of image_item ([`df812ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df812eaad5989f2930dde41d87491868505af946))
### Test
* test: add test for imageitem ([`88ecd05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/88ecd05b95974938ef1efff40e81854baf004cb4))
## v0.72.2 (2024-06-25)
### Fix
@@ -76,80 +142,12 @@
## v0.70.0 (2024-06-21)
### Documentation
* docs: fix typo in link ([`fdf11d8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fdf11d8147750e379af9b17792761a267b49ae53))
### Feature
* feat(bec-designer): automatic plugin discovery ([`4639eee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4639eee0b975ebd7a946e0e290449f5b88c372eb))
* feat(device_line_edit): plugin added to bec-designer ([`b4b27ae`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4b27aea3d8c08fa3d5d5514c69dbde32721d1dc))
* feat(device_combobox): plugin added to bec-designer ([`e483b28`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e483b282db20a81182b87938ea172654092419b5))
* feat: added entry point for bec-designer ([`36391db`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36391db60735d57b371211791ddf8d3d00cebcf1))
* feat(utils/bec-designer): added startup script to launched QtDesigner compatible with conda environments ([`5362334`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5362334ff3b07fc83653323a084a4b6946bade96))
### Fix
* fix(bec-desiger+plugins): imports fixed, PYSIDE6 check to not enable run plugins with pyqt6 ([`50b3422`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50b3422528d46d74317e8c903b6286e868ab7fe0))
## v0.69.0 (2024-06-21)
### Feature
* feat(widgets): added vscode widget ([`48ae950`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48ae950d57b454307ce409e2511f7b7adf3cfc6b))
### Fix
* fix(generate_cli): fixed rpc generate for classes without user access; closes #226 ([`925c893`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/925c893f3ff4337fc8b4d237c8ffc19a597b0996))
## v0.68.0 (2024-06-21)
### Feature
* feat: properly handle SIGINT (ctrl-c) in BEC GUI server -> calls qapplication.quit() ([`3644f34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3644f344da2df674bc0d5740c376a86b9d0dfe95))
* feat: bec-gui-server: redirect stdout and stderr (if any) as proper debug and error log entries ([`d1266a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1266a1ce148ff89557a039e3a182a87a3948f49))
* feat: add logger for BEC GUI server ([`630616e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/630616ec729f60aa0b4d17a9e0379f9c6198eb96))
### Fix
* fix: ignore GUI server output (any output will go to log file)
If a logger is given to log `_start_log_process`, the server stdout and
stderr streams will be redirected as log entries with levels DEBUG or ERROR
in their parent process ([`ce37416`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ce374163cab87a92847409051739777bc505a77b))
* fix: do not create 'BECClient' logger when instantiating BECDispatcher ([`f7d0b07`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f7d0b0768ace42a33e2556bb33611d4f02e5a6d9))
## v0.67.0 (2024-06-21)
### Documentation
* docs: add widget to documentation ([`6fa1c06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fa1c06053131dabd084bb3cf13c853b5d3ce833))
### Feature
* feat: introduce BECStatusBox Widget ([`443b6c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/443b6c1d7b02c772fda02e2d1eefd5bd40249e0c))
### Refactor
* refactor: Change inheritance to QTreeWidget from QWidget ([`d2f2b20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2f2b206bb0eab60b8a9b0d0ac60a6b7887fa6fb))
### Test
* test: add test suite for bec_status_box and status_item ([`5d4ca81`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4ca816cdedec4c88aba9eb326f85392504ea1c))
### Unknown
* Update file requirements.txt ([`505a5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/505a5ec8334ff4422913b3a7b79d39bcb42ad535))
## v0.66.1 (2024-06-20)
### Fix
* fix: fixed shutdown for pyside ([`2718bc6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2718bc624731301756df524d0d5beef6cb1c1430))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -13,6 +13,7 @@ class Widgets(str, enum.Enum):
Enum for the available widgets.
"""
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECDock = "BECDock"
BECDockArea = "BECDockArea"
@@ -31,6 +32,13 @@ class BECCurve(RPCBase):
Remove the curve from the plot.
"""
@property
@rpc_call
def dap_params(self):
"""
None
"""
@property
@rpc_call
def rpc_id(self) -> "str":
@@ -143,6 +151,13 @@ class BECCurve(RPCBase):
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
"""
@property
@rpc_call
def dap_params(self):
"""
None
"""
class BECDock(RPCBase):
@property
@@ -457,6 +472,7 @@ class BECFigure(RPCBase):
row: "int" = None,
col: "int" = None,
config=None,
dap: "str | None" = None,
**axis_kwargs,
) -> "BECWaveform":
"""
@@ -550,6 +566,7 @@ class BECFigure(RPCBase):
color_map_z: "str | None" = "plasma",
label: "str | None" = None,
validate: "bool" = True,
dap: "str | None" = None,
**axis_kwargs,
) -> "BECWaveform":
"""
@@ -568,6 +585,7 @@ class BECFigure(RPCBase):
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The DAP model to use for the curve.
**axis_kwargs: Additional axis properties to set on the widget after creation.
Returns:
@@ -711,6 +729,7 @@ class BECImageItem(RPCBase):
- log
- rot
- transpose
- autorange_mode
"""
@rpc_call
@@ -767,6 +786,15 @@ class BECImageItem(RPCBase):
autorange(bool): Whether to autorange the color bar.
"""
@rpc_call
def set_autorange_mode(self, mode: "Literal['max', 'mean']" = "mean"):
"""
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
Args:
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
"""
@rpc_call
def set_color_map(self, cmap: "str" = "magma"):
"""
@@ -796,7 +824,11 @@ class BECImageItem(RPCBase):
@rpc_call
def set_vrange(
self, vmin: "float" = None, vmax: "float" = None, vrange: "tuple[int, int]" = None
self,
vmin: "float" = None,
vmax: "float" = None,
vrange: "tuple[float, float]" = None,
change_autorange: "bool" = True,
):
"""
Set the range of the color bar.
@@ -931,6 +963,17 @@ class BECImageShow(RPCBase):
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_autorange_mode(self, mode: "Literal['max', 'mean']", name: "str" = None):
"""
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
Args:
mode(str): The autoscale mode of the image.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_monitor(self, monitor: "str", name: "str" = None):
"""
@@ -1164,6 +1207,13 @@ class BECImageShow(RPCBase):
class BECMotorMap(RPCBase):
@property
@rpc_call
def rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
"""
@property
@rpc_call
def config_dict(self) -> "dict":
@@ -1247,6 +1297,12 @@ class BECMotorMap(RPCBase):
dict: Data of the motor map.
"""
@rpc_call
def remove(self):
"""
Remove the plot widget from the figure.
"""
class BECPlotBase(RPCBase):
@property
@@ -1391,6 +1447,24 @@ class BECPlotBase(RPCBase):
"""
class BECQueue(RPCBase):
@property
@rpc_call
def config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class BECStatusBox(RPCBase):
@property
@rpc_call
@@ -1442,6 +1516,7 @@ class BECWaveform(RPCBase):
color_map_z: "str | None" = "plasma",
label: "str | None" = None,
validate: "bool" = True,
dap: "str | None" = None,
) -> "BECCurve":
"""
Plot a curve to the plot widget.
@@ -1458,11 +1533,50 @@ class BECWaveform(RPCBase):
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The dap model to use for the curve. If not specified, none will be added.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def add_dap(
self,
x_name: "str",
y_name: "str",
x_entry: "Optional[str]" = None,
y_entry: "Optional[str]" = None,
color: "Optional[str]" = None,
dap: "str" = "GaussianModel",
**kwargs,
) -> "BECCurve":
"""
Add LMFIT dap model curve to the plot widget.
Args:
x_name(str): Name of the x signal.
x_entry(str): Entry of the x signal.
y_name(str): Name of the y signal.
y_entry(str): Entry of the y signal.
color(str, optional): Color of the curve. Defaults to None.
color_map_z(str): The color map to use for the z-axis.
label(str, optional): Label of the curve. Defaults to None.
dap(str): The dap model to use for the curve.
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
@rpc_call
def remove_curve(self, *identifiers):
"""

View File

@@ -203,15 +203,12 @@ class BECGuiClientMixin:
if self._process is None:
return
self._run_rpc("close", (), wait_for_rpc_response=False)
while self.gui_is_alive():
time.sleep(0.2)
self._client.shutdown()
if self._process:
self._process.terminate()
if self._process_output_processing_thread:
self._process_output_processing_thread.join()
self._process.wait()
self._process = None

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import inspect
import signal
import sys
@@ -29,7 +31,7 @@ class BECWidgetsCLIServer:
dispatcher: BECDispatcher = None,
client=None,
config=None,
gui_class: Union["BECFigure", "BECDockArea"] = BECFigure,
gui_class: Union[BECFigure, BECDockArea] = BECFigure,
) -> None:
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
self.client = self.dispatcher.client if client is None else client
@@ -118,6 +120,7 @@ class BECWidgetsCLIServer:
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
self._shutdown_event = True
self._heartbeat_timer.stop()
self.gui.close()
self.client.shutdown()
@@ -207,6 +210,7 @@ def main():
app.quit()
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigint_handler)
sys.exit(app.exec())

View File

@@ -1,9 +0,0 @@
from .motor_movement import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -0,0 +1,96 @@
import os
import sys
import qdarktheme
from qtpy.QtGui import QActionGroup
from qtpy.QtWidgets import QStyle
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QMainWindow
from bec_widgets.examples.general_app.web_links import BECWebLinksMixin
from bec_widgets.utils.ui_loader import UILoader
class BECGeneralApp(QMainWindow):
def __init__(self, parent=None):
super(BECGeneralApp, self).__init__(parent)
ui_file_path = os.path.join(os.path.dirname(__file__), "general_app.ui")
self.load_ui(ui_file_path)
self.resize(1280, 720)
self.ini_ui()
def ini_ui(self):
self._setup_icons()
self._hook_menubar_docs()
self._hook_theme_bar()
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
def _hook_menubar_docs(self):
# BEC Docs
self.ui.action_BEC_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
# BEC Widgets Docs
self.ui.action_BEC_widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
# Bug report
self.ui.action_bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
def change_theme(self, theme):
qdarktheme.setup_theme(theme)
def _setup_icons(self):
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
computer_icon = QIcon.fromTheme("computer")
self.ui.action_BEC_docs.setIcon(help_icon)
self.ui.action_BEC_widgets_docs.setIcon(help_icon)
self.ui.action_bug_report.setIcon(bug_icon)
self.ui.central_tab.setTabIcon(0, computer_icon)
def _hook_theme_bar(self):
# Make actions checkable
self.ui.action_light.setCheckable(True)
self.ui.action_dark.setCheckable(True)
# Create an action group to make sure only one can be checked at a time
theme_group = QActionGroup(self)
theme_group.addAction(self.ui.action_light)
theme_group.addAction(self.ui.action_dark)
theme_group.setExclusive(True)
# Connect the actions to the theme change method
self.ui.action_light.triggered.connect(lambda: self.change_theme("light"))
self.ui.action_dark.triggered.connect(lambda: self.change_theme("dark"))
self.ui.action_dark.trigger()
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print(
"PYSIDE6 is not available in the environment. UI files with BEC custom widgets are runnable only with PySide6."
)
return
import bec_widgets
module_path = os.path.dirname(bec_widgets.__file__)
app = QApplication(sys.argv)
icon = QIcon()
icon.addFile(os.path.join(module_path, "assets", "BEC-Dark.png"), size=QSize(48, 48))
app.setWindowIcon(icon)
main_window = BECGeneralApp()
main_window.show()
sys.exit(app.exec_())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,380 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>1139</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="tabShape">
<enum>QTabWidget::TabShape::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTabWidget" name="central_tab">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="vscode">
<attribute name="icon">
<iconset theme="QIcon::ThemeIcon::Computer"/>
</attribute>
<attribute name="title">
<string>Visual Studio Code</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="VSCodeEditor" name="vscode"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Tab 2</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="action_BEC_docs"/>
<addaction name="action_BEC_widgets_docs"/>
<addaction name="action_bug_report"/>
</widget>
<widget class="QMenu" name="menuTheme">
<property name="title">
<string>Theme</string>
</property>
<addaction name="action_light"/>
<addaction name="action_dark"/>
</widget>
<addaction name="menuTheme"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dock_scan_control">
<property name="windowTitle">
<string>Scan Control</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ScanControl" name="scan_control"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_status_2">
<property name="windowTitle">
<string>BEC Service Status</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="BECStatusBox" name="bec_status_box_2">
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
<item>
<property name="flags">
<set>ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable</set>
</property>
</item>
</item>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_queue">
<property name="windowTitle">
<string>Scan Queue</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_4">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="BECQueue" name="bec_queue">
<row/>
<column/>
<column/>
<column/>
<item row="0" column="0"/>
<item row="0" column="1"/>
<item row="0" column="2"/>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="action_BEC_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Docs</string>
</property>
</action>
<action name="action_BEC_widgets_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Widgets Docs</string>
</property>
</action>
<action name="action_bug_report">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogError"/>
</property>
<property name="text">
<string>Bug Report</string>
</property>
</action>
<action name="action_light">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Light</string>
</property>
</action>
<action name="action_dark">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Dark</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>BECQueue</class>
<extends>QTableWidget</extends>
<header>bec_queue</header>
</customwidget>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>VSCodeEditor</class>
<extends>QWebEngineView</extends>
<header>vscode</header>
</customwidget>
<customwidget>
<class>BECStatusBox</class>
<extends>QTreeWidget</extends>
<header>bec_status_box</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends></extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,15 @@
import webbrowser
class BECWebLinksMixin:
@staticmethod
def open_bec_docs():
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
@staticmethod
def open_bec_widgets_docs():
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
@staticmethod
def open_bec_bug_report():
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")

View File

@@ -14,21 +14,6 @@ from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
# 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 JupyterConsoleWindow(QWidget): # pragma: no cover:
"""A widget that contains a Jupyter console linked to BEC Widgets with full API access (contains Qt and pyqtgraph API)."""
@@ -61,6 +46,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"fig0": self.fig0,
"fig1": self.fig1,
"fig2": self.fig2,
"plt": self.plt,
"bar": self.bar,
}
)
@@ -115,7 +101,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
self.fig2.plot(x_name="samx", y_name="bpm4i")
self.plt = self.fig2.plot(x_name="samx", y_name="bpm3a")
self.plt.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
self.bar.set_diameter(200)

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>2104</width>
<height>966</height>
<width>1415</width>
<height>832</height>
</rect>
</property>
<property name="windowTitle">
@@ -17,7 +17,7 @@
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">

View File

@@ -1,9 +0,0 @@
from .motor_control_compilations import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -9,5 +9,4 @@ from .crosshair import Crosshair
from .entry_validator import EntryValidator
from .layout_manager import GridLayoutManager
from .rpc_decorator import register_rpc_methods, rpc_public
from .ui_loader import UILoader
from .validator_delegate import DoubleValidationDelegate

View File

@@ -228,6 +228,7 @@ class BECConnector:
all_connections = self.rpc_register.list_all_connections()
if len(all_connections) == 0:
print("No more connections. Shutting down GUI BEC client.")
self.bec_dispatcher.disconnect_all()
self.client.shutdown()
# def closeEvent(self, event):

View File

@@ -21,12 +21,12 @@ if PYSIDE6:
import bec_widgets
def list_editable_packages() -> list[tuple[str, str]]:
def list_editable_packages() -> set[str]:
"""
List all editable packages in the environment.
Returns:
list[tuple[str, str]]: A list of tuples containing the package name and the path to the package.
set: A set of paths to editable packages.
"""
editable_packages = set()

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import argparse
import collections
from collections.abc import Callable
from typing import TYPE_CHECKING, Union

View File

@@ -1,6 +1,31 @@
from qtpy import QT_VERSION
from qtpy import PYQT6, PYSIDE6, QT_VERSION
from qtpy.QtCore import QFile, QIODevice
if PYSIDE6:
from PySide6.QtUiTools import QUiLoader
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.utils.plugin_utils import get_rpc_classes
class CustomUiLoader(QUiLoader):
def __init__(self, baseinstance):
super().__init__(baseinstance)
widgets = get_rpc_classes("bec_widgets").get("top_level_classes", [])
# remove this line once the plugin is not needed anymore
widgets.append(TicTacToe)
self.custom_widgets = {widget.__name__: widget for widget in widgets}
self.baseinstance = baseinstance
def createWidget(self, class_name, parent=None, name=""):
if class_name in self.custom_widgets:
widget = self.custom_widgets[class_name](parent)
widget.setObjectName(name)
return widget
return super().createWidget(class_name, parent, name)
class UILoader:
"""Universal UI loader for PyQt5, PyQt6, PySide2, and PySide6."""
@@ -14,14 +39,14 @@ class UILoader:
self.loader = uic.loadUi
elif QT_VERSION.startswith("6"):
# PyQt6 or PySide6
try:
from PySide6.QtUiTools import QUiLoader
if PYSIDE6:
self.loader = self.load_ui_pyside6
except ImportError:
elif PYQT6:
from PyQt6.uic import loadUi
self.loader = loadUi
else:
raise ImportError("No compatible Qt bindings found.")
def load_ui_pyside6(self, ui_file, parent=None):
"""
@@ -33,9 +58,8 @@ class UILoader:
Returns:
QWidget: The loaded widget.
"""
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader(parent)
loader = CustomUiLoader(parent)
file = QFile(ui_file)
if not file.open(QIODevice.ReadOnly):
raise IOError(f"Cannot open file: {ui_file}")

View File

@@ -0,0 +1,111 @@
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
class BECQueue(BECConnector, QTableWidget):
"""
Widget to display the BEC queue.
"""
def __init__(
self,
parent: QWidget | None = None,
client=None,
config: ConnectionConfig = None,
gui_id: str = None,
):
super().__init__(client, config, gui_id)
QTableWidget.__init__(self, parent=parent)
self.setColumnCount(3)
self.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
header = self.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
self.reset_content()
@Slot(dict, dict)
def update_queue(self, content, _metadata):
"""
Update the queue table with the latest queue information.
Args:
content (dict): The queue content.
_metadata (dict): The metadata.
"""
# only show the primary queue for now
queue_info = content.get("queue", {}).get("primary", {}).get("info", [])
self.setRowCount(len(queue_info))
self.clearContents()
if not queue_info:
self.reset_content()
return
for index, item in enumerate(queue_info):
blocks = item.get("request_blocks", [])
scan_types = []
scan_numbers = []
status = item.get("status", "")
for request_block in blocks:
scan_type = request_block.get("content", {}).get("scan_type", "")
if scan_type:
scan_types.append(scan_type)
scan_number = request_block.get("scan_number", "")
if scan_number:
scan_numbers.append(str(scan_number))
if scan_types:
scan_types = ", ".join(scan_types)
if scan_numbers:
scan_numbers = ", ".join(scan_numbers)
self.set_row(index, scan_numbers, scan_types, status)
def format_item(self, content: str) -> QTableWidgetItem:
"""
Format the content of the table item.
Args:
content (str): The content to be formatted.
Returns:
QTableWidgetItem: The formatted item.
"""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
return item
def set_row(self, index: int, scan_number: str, scan_type: str, status: str):
"""
Set the row of the table.
Args:
index (int): The index of the row.
scan_number (str): The scan number.
scan_type (str): The scan type.
status (str): The status.
"""
self.setItem(index, 0, self.format_item(scan_number))
self.setItem(index, 1, self.format_item(scan_type))
self.setItem(index, 2, self.format_item(status))
def reset_content(self):
"""
Reset the content of the table.
"""
self.setRowCount(1)
self.set_row(0, "", "", "")
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = BECQueue()
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1 @@
{'files': ['bec_queue.py']}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
DOM_XML = """
<ui language='c++'>
<widget class='BECQueue' name='bec_queue'>
</widget>
</ui>
"""
class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = BECQueue(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "bec_queue"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "BECQueue"
def toolTip(self):
return "Widget to display the BEC queue."
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.bec_queue.bec_queue_plugin import BECQueuePlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECQueuePlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,4 @@
{
"files": ["bec_status_box.py", "status_item.py",
]
}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.bec_status_box.bec_status_box import BECStatusBox
DOM_XML = """
<ui language='c++'>
<widget class='BECStatusBox' name='bec_status_box'>
</widget>
</ui>
"""
class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = BECStatusBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "bec_status_box"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "BECStatusBox"
def toolTip(self):
return "BECStatusBox widget for monitoring bec services"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.bec_status_box.bec_status_box_plugin import BECStatusBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECStatusBoxPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -195,10 +195,11 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
z_entry: str = None,
x: list | np.ndarray = None,
y: list | np.ndarray = None,
color: Optional[str] = None,
color_map_z: Optional[str] = "plasma",
label: Optional[str] = None,
color: str | None = None,
color_map_z: str | None = "plasma",
label: str | None = None,
validate: bool = True,
dap: str | None = None,
):
"""
Configure the waveform based on the provided parameters.
@@ -217,6 +218,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z (str): The color map to use for the z-axis.
label (str): The label of the curve.
validate (bool): If True, validate the device names and entries.
dap (str): The DAP model to use for the curve.
"""
if x is not None and y is None:
if isinstance(x, np.ndarray):
@@ -240,7 +242,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
return waveform
# User wants to add scan curve -> 1D Waveform
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
waveform.add_curve_scan(
waveform.plot(
x_name=x_name,
y_name=y_name,
x_entry=x_entry,
@@ -248,6 +250,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
validate=validate,
color=color,
label=label,
dap=dap,
)
# User wants to add scan curve -> 2D Waveform Scatter
if (
@@ -257,7 +260,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
and x is None
and y is None
):
waveform.add_curve_scan(
waveform.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
@@ -268,6 +271,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
# User wants to add custom curve
elif x is not None and y is not None and x_name is None and y_name is None:
@@ -292,6 +296,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
row: int = None,
col: int = None,
config=None,
dap: str | None = None,
**axis_kwargs,
) -> BECWaveform:
"""
@@ -339,6 +344,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
return waveform
@@ -357,6 +363,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z: str | None = "plasma",
label: str | None = None,
validate: bool = True,
dap: str | None = None,
**axis_kwargs,
) -> BECWaveform:
"""
@@ -375,6 +382,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The DAP model to use for the curve.
**axis_kwargs: Additional axis properties to set on the widget after creation.
Returns:
@@ -403,6 +411,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
# TODO remove repetition from .plot method
return waveform

View File

@@ -12,7 +12,11 @@ from qtpy.QtWidgets import QWidget
from bec_widgets.utils import EntryValidator
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
from bec_widgets.widgets.figure.plots.image.image_processor import ImageProcessor, ProcessorWorker
from bec_widgets.widgets.figure.plots.image.image_processor import (
ImageProcessor,
ImageStats,
ProcessorWorker,
)
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
@@ -35,6 +39,7 @@ class BECImageShow(BECPlotBase):
"set_vrange",
"set_color_map",
"set_autorange",
"set_autorange_mode",
"set_monitor",
"set_processing",
"set_image_properties",
@@ -86,6 +91,7 @@ class BECImageShow(BECPlotBase):
# Connect signals and slots
thread.started.connect(lambda: worker.process_image(device, image))
worker.processed.connect(self.update_image)
worker.stats.connect(self.update_vrange)
worker.finished.connect(thread.quit)
worker.finished.connect(thread.wait)
worker.finished.connect(worker.deleteLater)
@@ -341,6 +347,17 @@ class BECImageShow(BECPlotBase):
"""
self.apply_setting_to_images("set_autorange", args=[enable], kwargs={}, image_id=name)
def set_autorange_mode(self, mode: Literal["max", "mean"], name: str = None):
"""
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
Args:
mode(str): The autoscale mode of the image.
name(str): The name of the image. If None, apply to all images.
"""
self.apply_setting_to_images("set_autorange_mode", args=[mode], kwargs={}, image_id=name)
def set_monitor(self, monitor: str, name: str = None):
"""
Set the monitor of the image.
@@ -461,6 +478,7 @@ class BECImageShow(BECPlotBase):
else:
data = self.processor.process_image(data)
self.update_image(device, data)
self.update_vrange(device, self.processor.config.stats)
@pyqtSlot(str, np.ndarray)
def update_image(self, device: str, data: np.ndarray):
@@ -474,6 +492,18 @@ class BECImageShow(BECPlotBase):
image_to_update = self._images["device_monitor"][device]
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
@pyqtSlot(str, ImageStats)
def update_vrange(self, device: str, stats: ImageStats):
"""
Update the scaling of the image.
Args:
stats(ImageStats): The statistics of the image.
"""
image_to_update = self._images["device_monitor"][device]
if image_to_update.config.autorange:
image_to_update.auto_update_vrange(stats)
def _connect_device_monitor(self, monitor: str):
"""
Connect to the device monitor.

View File

@@ -7,7 +7,7 @@ import pyqtgraph as pg
from pydantic import Field
from bec_widgets.utils import BECConnector, ConnectionConfig
from bec_widgets.widgets.figure.plots.image.image_processor import ProcessingConfig
from bec_widgets.widgets.figure.plots.image.image_processor import ImageStats, ProcessingConfig
if TYPE_CHECKING:
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
@@ -20,13 +20,16 @@ class ImageItemConfig(ConnectionConfig):
color_map: Optional[str] = Field("magma", description="The color map of the image.")
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
vrange: Optional[tuple[int, int]] = Field(
vrange: Optional[tuple[float, float]] = Field(
None, description="The range of the color bar. If None, the range is automatically set."
)
color_bar: Optional[Literal["simple", "full"]] = Field(
"simple", description="The type of the color bar."
)
autorange: Optional[bool] = Field(True, description="Whether to autorange the color bar.")
autorange_mode: Optional[Literal["max", "mean"]] = Field(
"mean", description="Whether to use the mean of the image for autoscaling."
)
processing: ProcessingConfig = Field(
default_factory=ProcessingConfig, description="The post processing of the image."
)
@@ -43,6 +46,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
"set_transpose",
"set_opacity",
"set_autorange",
"set_autorange_mode",
"set_color_map",
"set_auto_downsample",
"set_monitor",
@@ -101,6 +105,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
- log
- rot
- transpose
- autorange_mode
"""
method_map = {
"downsample": self.set_auto_downsample,
@@ -112,6 +117,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
"log": self.set_log,
"rot": self.set_rotation,
"transpose": self.set_transpose,
"autorange_mode": self.set_autorange_mode,
}
for key, value in kwargs.items():
if key in method_map:
@@ -175,9 +181,18 @@ class BECImageItem(BECConnector, pg.ImageItem):
autorange(bool): Whether to autorange the color bar.
"""
self.config.autorange = autorange
if self.color_bar is not None:
if self.color_bar and autorange:
self.color_bar.autoHistogramRange()
def set_autorange_mode(self, mode: Literal["max", "mean"] = "mean"):
"""
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
Args:
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
"""
self.config.autorange_mode = mode
def set_color_map(self, cmap: str = "magma"):
"""
Set the color map of the image.
@@ -212,7 +227,29 @@ class BECImageItem(BECConnector, pg.ImageItem):
"""
self.config.monitor = monitor
def set_vrange(self, vmin: float = None, vmax: float = None, vrange: tuple[int, int] = None):
def auto_update_vrange(self, stats: ImageStats) -> None:
"""Auto update of the vrange base on the stats of the image.
Args:
stats(ImageStats): The stats of the image.
"""
fumble_factor = 2
if self.config.autorange_mode == "mean":
vmin = max(stats.mean - fumble_factor * stats.std, 0)
vmax = stats.mean + fumble_factor * stats.std
self.set_vrange(vmin, vmax, change_autorange=False)
return
if self.config.autorange_mode == "max":
self.set_vrange(max(stats.minimum, 0), stats.maximum, change_autorange=False)
return
def set_vrange(
self,
vmin: float = None,
vmax: float = None,
vrange: tuple[float, float] = None,
change_autorange: bool = True,
):
"""
Set the range of the color bar.
@@ -224,11 +261,13 @@ class BECImageItem(BECConnector, pg.ImageItem):
vmin, vmax = vrange
self.setLevels([vmin, vmax])
self.config.vrange = (vmin, vmax)
self.config.autorange = False
if change_autorange:
self.config.autorange = False
if self.color_bar is not None:
if self.config.color_bar == "simple":
self.color_bar.setLevels(low=vmin, high=vmax)
elif self.config.color_bar == "full":
# pylint: disable=unexpected-keyword-arg
self.color_bar.setLevels(min=vmin, max=vmax)
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import numpy as np
@@ -7,6 +8,16 @@ from pydantic import BaseModel, Field
from qtpy.QtCore import QObject, Signal, Slot
@dataclass
class ImageStats:
"""Container to store stats of an image."""
maximum: float
minimum: float
mean: float
std: float
class ProcessingConfig(BaseModel):
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
@@ -20,6 +31,10 @@ class ProcessingConfig(BaseModel):
None, description="The rotation angle of the monitor data before displaying."
)
model_config: dict = {"validate_assignment": True}
stats: ImageStats = Field(
ImageStats(maximum=0, minimum=0, mean=0, std=0),
description="The statistics of the image data.",
)
class ImageProcessor:
@@ -97,6 +112,18 @@ class ImageProcessor:
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
# return np.unravel_index(np.argmax(data), data.shape)
def update_image_stats(self, data: np.ndarray) -> None:
"""Get the statistics of the image data.
Args:
data(np.ndarray): The image data.
"""
self.config.stats.maximum = np.max(data)
self.config.stats.minimum = np.min(data)
self.config.stats.mean = np.mean(data)
self.config.stats.std = np.std(data)
def process_image(self, data: np.ndarray) -> np.ndarray:
"""
Process the data according to the configuration.
@@ -115,6 +142,7 @@ class ImageProcessor:
data = self.transpose(data)
if self.config.log:
data = self.log(data)
self.update_image_stats(data)
return data
@@ -124,6 +152,7 @@ class ProcessorWorker(QObject):
"""
processed = Signal(str, np.ndarray)
stats = Signal(str, ImageStats)
stopRequested = Signal()
finished = Signal()
@@ -147,6 +176,7 @@ class ProcessorWorker(QObject):
self._isRunning = False
if not self._isRunning:
self.processed.emit(device, processed_image)
self.stats.emit(self.processor.config.stats)
self.finished.emit()
def stop(self):

View File

@@ -36,6 +36,7 @@ class MotorMapConfig(SubplotConfig):
class BECMotorMap(BECPlotBase):
USER_ACCESS = [
"rpc_id",
"config_dict",
"change_motors",
"set_max_points",
@@ -44,6 +45,7 @@ class BECMotorMap(BECPlotBase):
"set_background_value",
"set_scatter_size",
"get_data",
"remove",
]
# QT Signals

View File

@@ -1,10 +1,12 @@
from __future__ import annotations
import time
from collections import defaultdict
from typing import Any, Literal, Optional
import numpy as np
import pyqtgraph as pg
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.scan_data import ScanData
from pydantic import Field, ValidationError
@@ -36,6 +38,8 @@ class BECWaveform(BECPlotBase):
"rpc_id",
"config_dict",
"plot",
"add_dap",
"get_dap_params",
"remove_curve",
"scan_history",
"curves",
@@ -57,6 +61,7 @@ class BECWaveform(BECPlotBase):
"set_legend_label_size",
]
scan_signal_update = pyqtSignal()
dap_params_update = pyqtSignal(dict)
def __init__(
self,
@@ -73,6 +78,7 @@ class BECWaveform(BECPlotBase):
)
self._curves_data = defaultdict(dict)
self.old_scan_id = None
self.scan_id = None
# Scan segment update proxy
@@ -80,6 +86,9 @@ class BECWaveform(BECPlotBase):
self.scan_signal_update, rateLimit=25, slot=self._update_scan_segment_plot
)
self.proxy_update_dap = pg.SignalProxy(
self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
)
# Get bec shortcuts dev, scans, queue, scan_storage, dap
self.get_bec_shortcuts()
@@ -213,6 +222,7 @@ class BECWaveform(BECPlotBase):
color_map_z: str | None = "plasma",
label: str | None = None,
validate: bool = True,
dap: str | None = None, # TODO add dap custom curve wrapper
) -> BECCurve:
"""
Plot a curve to the plot widget.
@@ -229,6 +239,7 @@ class BECWaveform(BECPlotBase):
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The dap model to use for the curve. If not specified, none will be added.
Returns:
BECCurve: The curve object.
@@ -237,6 +248,8 @@ class BECWaveform(BECPlotBase):
if x is not None and y is not None:
return self.add_curve_custom(x=x, y=y, label=label, color=color)
else:
if dap:
self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
return self.add_curve_scan(
x_name=x_name,
y_name=y_name,
@@ -256,6 +269,7 @@ class BECWaveform(BECPlotBase):
y: list | np.ndarray,
label: str = None,
color: str = None,
curve_source: str = "custom",
**kwargs,
) -> BECCurve:
"""
@@ -266,12 +280,13 @@ class BECWaveform(BECPlotBase):
y(list|np.ndarray): Y data of the curve.
label(str, optional): Label of the curve. Defaults to None.
color(str, optional): Color of the curve. Defaults to None.
curve_source(str, optional): Tag for source of the curve. Defaults to "custom".
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
curve_source = "custom"
curve_source = curve_source
curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
curve_exits = self._check_curve_id(curve_id, self._curves_data)
@@ -314,10 +329,12 @@ class BECWaveform(BECPlotBase):
color_map_z: Optional[str] = "plasma",
label: Optional[str] = None,
validate_bec: bool = True,
source: str = "scan_segment",
dap: Optional[str] = None,
**kwargs,
) -> BECCurve:
"""
Add a curve to the plot widget from the scan segment.
Add a curve to the plot widget from the scan segment. #TODO adapt docs to DAP
Args:
x_name(str): Name of the x signal.
@@ -335,7 +352,7 @@ class BECWaveform(BECPlotBase):
BECCurve: The curve object.
"""
# Check if curve already exists
curve_source = "scan_segment"
curve_source = source
# Get entry if not provided and validate
x_entry, y_entry, z_entry = self._validate_signal_entries(
@@ -371,12 +388,74 @@ class BECWaveform(BECPlotBase):
x=SignalData(name=x_name, entry=x_entry),
y=SignalData(name=y_name, entry=y_entry),
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
dap=dap,
),
**kwargs,
)
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
return curve
def add_dap(
self,
x_name: str,
y_name: str,
x_entry: Optional[str] = None,
y_entry: Optional[str] = None,
color: Optional[str] = None,
dap: str = "GaussianModel",
**kwargs,
) -> BECCurve:
"""
Add LMFIT dap model curve to the plot widget.
Args:
x_name(str): Name of the x signal.
x_entry(str): Entry of the x signal.
y_name(str): Name of the y signal.
y_entry(str): Entry of the y signal.
color(str, optional): Color of the curve. Defaults to None.
color_map_z(str): The color map to use for the z-axis.
label(str, optional): Label of the curve. Defaults to None.
dap(str): The dap model to use for the curve.
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
x_entry, y_entry, _ = self._validate_signal_entries(
x_name, y_name, None, x_entry, y_entry, None
)
label = f"{y_name}-{y_entry}-{dap}"
curve = self.add_curve_scan(
x_name=x_name,
y_name=y_name,
x_entry=x_entry,
y_entry=y_entry,
color=color,
label=label,
source="DAP",
dap=dap,
pen_style="dash",
symbol="star",
**kwargs,
)
self.setup_dap(self.old_scan_id, self.scan_id)
self.refresh_dap()
return curve
def get_dap_params(self) -> dict:
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
params = {}
for curve_id, curve in self._curves_data["DAP"].items():
params[curve_id] = curve.dap_params
return params
def _add_curve_object(
self,
name: str,
@@ -528,13 +607,75 @@ class BECWaveform(BECPlotBase):
return
if current_scan_id != self.scan_id:
self.old_scan_id = self.scan_id
self.scan_id = current_scan_id
self.scan_segment_data = self.queue.scan_storage.find_scan_by_ID(
self.scan_id
) # TODO do scan access through BECFigure
self.setup_dap(self.old_scan_id, self.scan_id)
self.scan_signal_update.emit()
def setup_dap(self, old_scan_id, new_scan_id):
"""
Setup DAP for the new scan.
Args:
old_scan_id(str): old_scan_id, used to disconnect the previous dispatcher connection.
new_scan_id(str): new_scan_id, used to connect the new dispatcher connection.
"""
self.bec_dispatcher.disconnect_slot(
self.update_dap, MessageEndpoints.dap_response(old_scan_id)
)
if len(self._curves_data["DAP"]) > 0:
self.bec_dispatcher.connect_slot(
self.update_dap, MessageEndpoints.dap_response(new_scan_id)
)
def refresh_dap(self):
"""
Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
"""
for curve_id, curve in self._curves_data["DAP"].items():
x_name = curve.config.signals.x.name
y_name = curve.config.signals.y.name
x_entry = curve.config.signals.x.entry
y_entry = curve.config.signals.y.entry
model_name = curve.config.signals.dap
model = getattr(self.dap, model_name)
msg = messages.DAPRequestMessage(
dap_cls="LmfitService1D",
dap_type="on_demand",
config={
"args": [self.scan_id, x_name, x_entry, y_name, y_entry],
"kwargs": {},
"class_args": model._plugin_info["class_args"],
"class_kwargs": model._plugin_info["class_kwargs"],
},
metadata={"RID": self.scan_id},
)
self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
@pyqtSlot(dict, dict)
def update_dap(self, msg, metadata):
self.msg = msg
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
curve_id_request = f"{y_name}-{y_entry}-{model}"
for curve_id, curve in self._curves_data["DAP"].items():
if curve_id == curve_id_request:
if msg["data"] is not None:
x = msg["data"][0]["x"]
y = msg["data"][0]["y"]
curve.setData(x, y)
curve.dap_params = msg["data"][1]["fit_parameters"]
self.dap_params_update.emit(curve.dap_params)
break
def _update_scan_segment_plot(self):
"""Update the plot with the data from the scan segment."""
data = self.scan_segment_data.data
@@ -609,13 +750,17 @@ class BECWaveform(BECPlotBase):
if scan_index is not None and scan_id is not None:
raise ValueError("Only one of scan_id or scan_index can be provided.")
# Reset DAP connector
self.bec_dispatcher.disconnect_slot(
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
)
if scan_index is not None:
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
elif scan_id is not None:
self.scan_id = scan_id
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
self.setup_dap(self.old_scan_id, self.scan_id)
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
self._update_scan_curves(data)
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
@@ -661,6 +806,9 @@ class BECWaveform(BECPlotBase):
def cleanup(self):
"""Cleanup the widget connection from BECDispatcher."""
self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
self.bec_dispatcher.disconnect_slot(
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
)
for curve in self.curves:
curve.cleanup()
super().cleanup()

View File

@@ -31,6 +31,7 @@ class Signal(BaseModel):
x: SignalData # TODO maybe add metadata for config gui later
y: SignalData
z: Optional[SignalData] = None
dap: Optional[str] = None
model_config: dict = {"validate_assignment": True}
@@ -63,6 +64,7 @@ class CurveConfig(ConnectionConfig):
class BECCurve(BECConnector, pg.PlotDataItem):
USER_ACCESS = [
"remove",
"dap_params",
"rpc_id",
"config_dict",
"set",
@@ -75,6 +77,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
"set_pen_width",
"set_pen_style",
"get_data",
"dap_params",
]
def __init__(
@@ -96,6 +99,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
self.parent_item = parent_item
self.apply_config()
self.dap_params = None
if kwargs:
self.set(**kwargs)
@@ -119,6 +123,14 @@ class BECCurve(BECConnector, pg.PlotDataItem):
self.setSymbolSize(self.config.symbol_size)
self.setSymbol(self.config.symbol)
@property
def dap_params(self):
return self._dap_params
@dap_params.setter
def dap_params(self, value):
self._dap_params = value
def set_data(self, x, y):
if self.config.source == "custom":
self.setData(x, y)
@@ -241,5 +253,6 @@ class BECCurve(BECConnector, pg.PlotDataItem):
def remove(self):
"""Remove the curve from the plot."""
self.parent_item.removeItem(self)
# self.parent_item.removeItem(self)
self.parent_item.remove_curve(self.name())
self.cleanup()

View File

@@ -2,16 +2,16 @@ from bec_ipython_client.main import BECIPythonClient
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.manager import QtKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtWidgets import QApplication, QMainWindow
from qtpy.QtWidgets import QApplication
class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
def __init__(self, inprocess: bool = False):
super().__init__()
def __init__(self, parent=None, inprocess=True):
super().__init__(parent=parent)
self.inprocess = None
self.inprocess = inprocess
self.kernel_manager, self.kernel_client = self._init_kernel(inprocess=inprocess)
self.kernel_manager, self.kernel_client = self._init_kernel(inprocess=self.inprocess)
self.set_default_style("linux")
self._init_bec()
@@ -61,12 +61,10 @@ class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
self.kernel_manager.shutdown_kernel()
if __name__ == "__main__": # pragma: no cover
import sys
app = QApplication(sys.argv)
win = QMainWindow()
win.setCentralWidget(BECJupyterConsole(True))
win.show()
sys.exit(app.exec_())
# if __name__ == "__main__": # pragma: no cover
# import sys
#
# app = QApplication(sys.argv)
# win = BECJupyterConsole(inprocess=True)
# win.show()
# sys.exit(app.exec_())

View File

@@ -0,0 +1,3 @@
{
"files": ["jupyter_console.py"]
}

View File

@@ -0,0 +1,51 @@
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
DOM_XML = """
<ui language='c++'>
<widget class='BECJupyterConsole' name='bec_jupyter'>
</widget>
</ui>
"""
class BECJupyterConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = BECJupyterConsole(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "bec_jupyter"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "BECJupyterConsole"
def toolTip(self):
return "BECJupyterConsole widget"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.jupyter_console.jupyter_console_plugin import BECJupyterConsolePlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECJupyterConsolePlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -16,7 +16,7 @@ from qtpy.QtWidgets import (
QTableWidgetItem,
)
from bec_widgets.utils import UILoader
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget

View File

@@ -5,7 +5,7 @@ from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget

View File

@@ -7,7 +7,7 @@ from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Literal, Optional
from bec_lib.endpoints import EndpointInfo
from bec_lib.endpoints import EndpointInfo, MessageEndpoints
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtGui
@@ -21,14 +21,14 @@ class ProgressbarConnections(BaseModel):
slot = values.data["slot"]
v = v.endpoint if isinstance(v, EndpointInfo) else v
if slot == "on_scan_progress":
if v != "scans/scan_progress":
if v != MessageEndpoints.scan_progress().endpoint:
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
{"wrong_value": v},
)
elif slot == "on_device_readback":
if not v.startswith("internal/devices/readback/"):
if not v.startswith(MessageEndpoints.device_readback("").endpoint):
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
@@ -135,6 +135,7 @@ class Ring(BECConnector):
float(max(self.config.min_value, min(self.config.max_value, value))),
self.config.precision,
)
self.parent_progress_widget.update()
def set_color(self, color: str | tuple):
"""
@@ -145,6 +146,7 @@ class Ring(BECConnector):
"""
self.config.color = color
self.color = self.convert_color(color)
self.parent_progress_widget.update()
def set_background(self, color: str | tuple):
"""
@@ -155,6 +157,7 @@ class Ring(BECConnector):
"""
self.config.background_color = color
self.color = self.convert_color(color)
self.parent_progress_widget.update()
def set_line_width(self, width: int):
"""
@@ -164,6 +167,7 @@ class Ring(BECConnector):
width(int): Line width for the ring widget
"""
self.config.line_width = width
self.parent_progress_widget.update()
def set_min_max_values(self, min_value: int | float, max_value: int | float):
"""
@@ -175,6 +179,7 @@ class Ring(BECConnector):
"""
self.config.min_value = min_value
self.config.max_value = max_value
self.parent_progress_widget.update()
def set_start_angle(self, start_angle: int):
"""
@@ -185,6 +190,7 @@ class Ring(BECConnector):
"""
self.config.start_position = start_angle
self.start_position = start_angle * 16
self.parent_progress_widget.update()
@staticmethod
def convert_color(color):

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.scan_control.scan_control_plugin import ScanControlPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ScanControlPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,3 @@
{
"files": ["scan_control.py","scan_control_box.py"]
}

View File

@@ -0,0 +1,51 @@
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.scan_control import ScanControl
DOM_XML = """
<ui language='c++'>
<widget class='ScanControl' name='scan_control'>
</widget>
</ui>
"""
class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = ScanControl(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "scan_control"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "ScanControl"
def toolTip(self):
return "ScanControl widget"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.vscode.vscode_plugin import VSCodeEditorPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(VSCodeEditorPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -60,7 +60,7 @@ class VSCodeEditor(WebsiteWidget):
"""
Cleanup the VSCode editor.
"""
if not self.process:
if not self.process or self.process.poll() is not None:
return
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.process.wait()
@@ -72,6 +72,13 @@ class VSCodeEditor(WebsiteWidget):
self.cleanup_vscode()
return super().cleanup()
def close(self):
"""
Close the widget.
"""
self.cleanup_vscode()
return super().close()
if __name__ == "__main__": # pragma: no cover
import sys

View File

@@ -0,0 +1,3 @@
{
"files": ["vscode.py"]
}

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.vscode.vscode import VSCodeEditor
DOM_XML = """
<ui language='c++'>
<widget class='VSCodeEditor' name='vscode'>
</widget>
</ui>
"""
class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = VSCodeEditor(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "vscode"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "VSCodeEditor"
def toolTip(self):
return "VSCodeEditor widget"
def whatsThis(self):
return self.toolTip()

View File

@@ -7,7 +7,6 @@ In the following, we describe 4 different type of widgets thaat are available in
![BECFigure.png](BECFigure.png)
(user.widgets.waveform_1d)=
## [1D Waveform Widget](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform)
**Purpose:** This widget provides a straightforward visualization of 1D data. It is particularly useful for plotting positioner movements against detector readings, enabling users to observe correlations and patterns in a simple, linear format.
@@ -20,11 +19,12 @@ In the following, we describe 4 different type of widgets thaat are available in
**Example of Use:**
![Waveform 1D](./w1D.gif)
**Code example**
**Code example 1 - adding curves**
The following code snipped demonstrates how to create a 1D waveform plot using BEC Widgets within BEC. More details about BEC Widgets in BEC can be found in the getting started section within the [introduction to the command line.](user.command_line_introduction)
```python
# adds a new dock, a new BECFigure and a BECWaveForm to the dock
plt = gui.add_dock().add_widget('BECFigure').plot('samx', 'bpm4i')
plt = gui.add_dock().add_widget('BECFigure').plot(x_name='samx', y_name='bpm4i')
# add a second curve to the same plot
plt.plot(x_name='samx', y_name='bpm3i')
plt.set_title("Gauss plots vs. samx")
@@ -39,6 +39,48 @@ dev.bpm4i.sim.select_sim_model("GaussianModel")
dev.bpm3i.sim.select_sim_model("StepModel")
```
**Code example 2 - Adding Data Processing Pipeline Curve with LMFit Models**
Together with the scan curve, one can also add a second curve that fits the signal using a specified model
from [LMFit](https://lmfit.github.io/lmfit-py/builtin_models.html). The following code snippet demonstrates how to
create a 1D waveform curve with an attached DAP process, or how to add a DAP process to an existing curve using the BEC
CLI. Please note that for this example, both devices were set as Gaussian signals.
```python
# Add a new dock, a new BECFigure, and a BECWaveForm to the dock with a GaussianModel DAP
plt = gui.add_dock().add_widget('BECFigure').plot(x_name='samx', y_name='bpm4i', dap="GaussianModel")
# Add a second curve to the same plot without DAP
plt.plot(x_name='samx', y_name='bpm3a')
# Add DAP to the second curve
plt.add_dap(x_name='samx', y_name='bpm3a', dap="GaussianModel")
```
To get the parameters of the fit, one has to retrieve the curve objects and call the dap_params property.
```python
# Get the curve object by name from the legend
dap_bpm4i = plt.get_curve("bpm4i-bpm4i-GaussianModel")
dap_bpm3a = plt.get_curve("bpm3a-bpm3a-GaussianModel")
# Get the parameters of the fit
print(dap_bpm4i.dap_params)
# Output
{'amplitude': 197.399639720862,
'center': 5.013486095404885,
'sigma': 0.9820868875739888}
print(dap_bpm3a.dap_params)
# Output
{'amplitude': 698.3072786185278,
'center': 0.9702840866173836,
'sigma': 1.97139754785518}
```
![Waveform 1D_DAP](./bec_figure_dap.gif)
(user.widgets.scatter_2d)=
## [2D Scatter Plot](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.72.2"
version = "0.74.1"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [
@@ -13,30 +13,28 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"pydantic",
"qtconsole",
"jedi",
"qtpy",
"pyqtgraph",
"bec_lib",
"bec_ipython_client", # needed for jupyter widget
"zmq",
"h5py",
"pyqtdarktheme",
"black", # needed for bw-generate-cli
"isort", # needed for bw-generate-cli
"bec_ipython_client~=2.16", # needed for jupyter console
"bec_lib~=2.16",
"black~=24.0", # needed for bw-generate-cli
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"pydantic~=2.0",
"pyqtgraph~=0.13",
"pyqtdarktheme~=2.1",
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtpy~=2.4",
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-random-order",
"pytest-timeout",
"pytest-xvfb",
"coverage",
"pytest-qt",
"fakeredis",
"coverage~=7.0",
"fakeredis~=2.23, >=2.23.2",
"pytest-bec-e2e~=2.16",
"pytest-qt~=4.4",
"pytest-random-order~=1.1",
"pytest-timeout~=2.2",
"pytest-xvfb~=3.0",
"pytest~=8.0",
]
pyqt5 = ["PyQt5>=5.9", "PyQtWebEngine>=5.9"]
pyqt6 = ["PyQt6>=6.7", "PyQt6-WebEngine>=6.7"]

View File

@@ -53,6 +53,7 @@ def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
assert im.__class__ == BECImageShow
assert mm.config_dict["signals"] == {
"dap": None,
"source": "device_readback",
"x": {
"name": "samx",
@@ -71,6 +72,7 @@ def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
"z": None,
}
assert plt.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},

View File

@@ -1,3 +1,5 @@
import time
import numpy as np
import pytest
from bec_lib.endpoints import MessageEndpoints
@@ -38,6 +40,7 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
# check if the correct devices are set
# plot
assert plt.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
@@ -47,6 +50,7 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
assert im.config_dict["images"]["eiger"]["monitor"] == "eiger"
# motor map
assert motor_map.config_dict["signals"] == {
"dap": None,
"source": "device_readback",
"x": {
"name": "samx",
@@ -66,6 +70,7 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
}
# plot with z scatter
assert plt_z.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "samy", "entry": "samy", "unit": None, "modifier": None, "limits": None},
@@ -151,3 +156,55 @@ def test_rpc_motor_map(rpc_server_figure, bec_client_lib):
np.testing.assert_equal(
[motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
)
def test_dap_rpc(rpc_server_figure, bec_client_lib):
fig = BECFigure(rpc_server_figure)
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
dev.bpm4i.sim.sim_select_model("GaussianModel")
params = dev.bpm4i.sim.sim_params
params.update(
{"noise": "uniform", "noise_multiplier": 10, "center": 5, "sigma": 1, "amplitude": 200}
)
dev.bpm4i.sim.sim_params = params
time.sleep(1)
res = scans.line_scan(dev.samx, 0, 8, steps=50, relative=False)
res.wait()
time.sleep(2)
dap_curve = plt.get_curve("bpm4i-bpm4i-GaussianModel")
fit_params = dap_curve.dap_params
print(fit_params)
assert np.isclose(fit_params["center"], 5, atol=0.5)
def test_removing_subplots(rpc_server_figure, bec_client_lib):
fig = BECFigure(rpc_server_figure)
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
im = fig.image(monitor="eiger")
mm = fig.motor_map(motor_x="samx", motor_y="samy")
assert len(fig.widget_list) == 3
# removing curves
assert len(plt.curves) == 2
plt.curves[0].remove()
assert len(plt.curves) == 1
plt.remove_curve("bpm4i-bpm4i")
assert len(plt.curves) == 0
# removing all subplots from figure
plt.remove()
im.remove()
mm.remove()
assert len(fig.widget_list) == 0

View File

@@ -0,0 +1,65 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
from unittest import mock
import numpy as np
import pytest
from bec_lib import messages
from qtpy.QtGui import QFontInfo
from .client_mocks import mocked_client
from .test_bec_figure import bec_figure
@pytest.fixture
def bec_image_show(bec_figure):
yield bec_figure.image("eiger")
def test_on_image_update(bec_image_show):
data = np.random.rand(100, 100)
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
def test_autorange_on_image_update(bec_image_show):
# Check if autorange mode "mean" works, should be default
data = np.random.rand(100, 100)
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
vmin = max(np.mean(data) - 2 * np.std(data), 0)
vmax = np.mean(data) + 2 * np.std(data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Test general update with autorange True, mode "max"
bec_image_show.set_autorange_mode("max")
bec_image_show.on_image_update(msg)
img = bec_image_show.images[0]
vmin = np.min(data)
vmax = np.max(data)
assert np.array_equal(img.get_data(), data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Change the input data, and switch to autorange False, colormap levels should stay untouched
data *= 100
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.set_autorange(False)
bec_image_show.on_image_update(msg)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-3, 1e-3)).all()
# Reactivate autorange, should now scale the new data
bec_image_show.set_autorange(True)
bec_image_show.set_autorange_mode("mean")
bec_image_show.on_image_update(msg)
img = bec_image_show.images[0]
vmin = max(np.mean(data) - 2 * np.std(data), 0)
vmax = np.mean(data) + 2 * np.std(data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()

View File

@@ -0,0 +1,111 @@
import pytest
from bec_lib import messages
from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
from .client_mocks import mocked_client
@pytest.fixture
def bec_queue_msg_full():
content = {
"primary": {
"info": [
{
"active_request_block": None,
"is_scan": [True],
"queue_id": "600163fc-5e56-4901-af25-14e9ee76817c",
"request_blocks": [
{
"RID": "89a76021-28c0-4297-828e-74ae40b941e5",
"content": {
"parameter": {
"args": {"samx": [-0.1, 0.1]},
"kwargs": {
"exp_time": 0.5,
"relative": True,
"steps": 20,
"system_config": {
"file_directory": None,
"file_suffix": None,
},
},
},
"queue": "primary",
"scan_type": "line_scan",
},
"is_scan": True,
"metadata": {
"RID": "89a76021-28c0-4297-828e-74ae40b941e5",
"file_directory": None,
"file_suffix": None,
"user_metadata": {"sample_name": "testA"},
},
"msg": messages.ScanQueueMessage(
metadata={
"file_suffix": None,
"file_directory": None,
"user_metadata": {"sample_name": "testA"},
"RID": "89a76021-28c0-4297-828e-74ae40b941e5",
},
scan_type="line_scan",
parameter={
"args": {"samx": [-0.1, 0.1]},
"kwargs": {
"steps": 20,
"exp_time": 0.5,
"relative": True,
"system_config": {
"file_suffix": None,
"file_directory": None,
},
},
},
queue="primary",
),
"readout_priority": {
"async": [],
"baseline": [],
"monitored": ["samx"],
"on_request": [],
},
"report_instructions": [{"scan_progress": 20}],
"scan_id": "2d704cc3-c172-404c-866d-608ce09fce40",
"scan_motors": ["samx"],
"scan_number": 1289,
}
],
"scan_id": ["2d704cc3-c172-404c-866d-608ce09fce40"],
"scan_number": [1289],
"status": "COMPLETED",
}
],
"status": "RUNNING",
}
}
msg = messages.ScanQueueStatusMessage(metadata={}, queue=content)
return msg
@pytest.fixture
def bec_queue(qtbot, mocked_client):
widget = BECQueue(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_bec_queue(bec_queue, bec_queue_msg_full):
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == "1289"
assert bec_queue.item(0, 1).text() == "line_scan"
assert bec_queue.item(0, 2).text() == "COMPLETED"
def test_bec_queue_empty(bec_queue):
bec_queue.update_queue({}, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == ""
assert bec_queue.item(0, 1).text() == ""
assert bec_queue.item(0, 2).text() == ""

View File

@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
from bec_lib.devicemanager import DeviceContainer
from bec_widgets.examples import (
from bec_widgets.examples.motor_movement.motor_control_compilations import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,

View File

@@ -46,7 +46,8 @@ def test_start_server(qtbot, mocked_client):
)
def test_close_event(qtbot, vscode_widget):
@pytest.fixture
def patched_vscode_process(qtbot, vscode_widget):
with mock.patch("bec_widgets.widgets.vscode.vscode.os.killpg") as mock_killpg:
with mock.patch("bec_widgets.widgets.vscode.vscode.os.getpgid") as mock_getpgid:
with mock.patch(
@@ -54,8 +55,24 @@ def test_close_event(qtbot, vscode_widget):
) as mock_close_event:
mock_getpgid.return_value = 123
vscode_widget.process = mock.Mock()
vscode_widget.process.pid = 123
vscode_widget.closeEvent(None)
mock_killpg.assert_called_once_with(123, 15)
vscode_widget.process.wait.assert_called_once()
mock_close_event.assert_called_once()
yield vscode_widget, mock_killpg, mock_close_event
def test_close_event(qtbot, patched_vscode_process):
vscode_patched, mock_killpg, mock_close_event = patched_vscode_process
vscode_patched.process.pid = 123
vscode_patched.process.poll.return_value = None
vscode_patched.closeEvent(None)
mock_killpg.assert_called_once_with(123, 15)
vscode_patched.process.wait.assert_called_once()
mock_close_event.assert_called_once()
def test_close_event_on_terminated_code(qtbot, patched_vscode_process):
vscode_patched, mock_killpg, mock_close_event = patched_vscode_process
vscode_patched.process.pid = 123
vscode_patched.process.poll.return_value = 0
vscode_patched.closeEvent(None)
mock_killpg.assert_not_called()
vscode_patched.process.wait.assert_not_called()
mock_close_event.assert_called_once()

View File

@@ -85,6 +85,7 @@ def test_create_waveform1D_by_config(bec_figure):
"pen_style": "dash",
"source": "scan_segment",
"signals": {
"dap": None,
"source": "scan_segment",
"x": {
"name": "samx",
@@ -248,6 +249,7 @@ def test_change_curve_appearance_methods(bec_figure, qtbot):
assert c1.config.pen_style == "dashdot"
assert c1.config.source == "scan_segment"
assert c1.config.signals.model_dump() == {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
@@ -277,6 +279,7 @@ def test_change_curve_appearance_args(bec_figure):
assert c1.config.pen_style == "dashdot"
assert c1.config.source == "scan_segment"
assert c1.config.signals.model_dump() == {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
@@ -384,6 +387,7 @@ def test_curve_add_by_config(bec_figure):
"pen_style": "dash",
"source": "scan_segment",
"signals": {
"dap": None,
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {