From dc5fd9959f6697a83694a9b923cea5996e2e87b1 Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Thu, 10 Aug 2023 18:12:21 +0200 Subject: [PATCH 01/11] refactor: made client a module import --- bec_widgets/line_plot.py | 18 ++--- tests/test_line_plot.py | 148 +++++++++++++++++++-------------------- tests/test_scan_plot.py | 51 ++++++++++---- 3 files changed, 122 insertions(+), 95 deletions(-) diff --git a/bec_widgets/line_plot.py b/bec_widgets/line_plot.py index 5fc7d830..306927ea 100644 --- a/bec_widgets/line_plot.py +++ b/bec_widgets/line_plot.py @@ -1,20 +1,22 @@ import os -import warnings -import time -from typing import Any import threading +import time +import warnings +from typing import Any + import numpy as np import pyqtgraph import pyqtgraph as pg +from bec_lib.core import BECMessage from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import QTableWidgetItem, QCheckBox - -from bec_lib import BECClient +from PyQt5.QtWidgets import QCheckBox, QTableWidgetItem from pyqtgraph import mkBrush, mkColor, mkPen from pyqtgraph.Qt import QtCore, QtWidgets, uic from pyqtgraph.Qt.QtCore import pyqtSignal -from bec_lib.core import BECMessage +from bec_widgets.bec_dispatcher import bec_dispatcher + +client = bec_dispatcher.client class BasicPlot(QtWidgets.QWidget): @@ -418,9 +420,9 @@ class BasicPlot(QtWidgets.QWidget): if __name__ == "__main__": import argparse - from bec_widgets.bec_dispatcher import bec_dispatcher from bec_widgets import ctrl_c + from bec_widgets.bec_dispatcher import bec_dispatcher parser = argparse.ArgumentParser() parser.add_argument( diff --git a/tests/test_line_plot.py b/tests/test_line_plot.py index a561cb34..c960a74a 100644 --- a/tests/test_line_plot.py +++ b/tests/test_line_plot.py @@ -19,9 +19,9 @@ def test_line_plot_emits_no_signal(qtbot): } } metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.BECClient") as mock_client: + with mock.patch("bec_widgets.line_plot.client") as mock_client: with mock.patch.object(plot, "update_signal") as mock_update_signal: - plot(data=data, metadata=metadata) + plot.on_scan_segment(data=data, metadata=metadata) mock_update_signal.emit.assert_not_called() @@ -39,12 +39,12 @@ def test_line_plot_emits_signal(qtbot): } plotter_data_y = [[1, 1], [3, 3]] metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.BECClient") as mock_client: + with mock.patch("bec_widgets.line_plot.client") as mock_client: # mock_client.device_manager.devices.keys.return_value = ["y1"] with mock.patch.object(plot, "update_signal") as mock_update_signal: mock_update_signal.emit() - plot(data=data, metadata=metadata) - plot(data=data, metadata=metadata) + plot.on_scan_segment(data=data, metadata=metadata) + plot.on_scan_segment(data=data, metadata=metadata) mock_update_signal.emit.assert_called() # TODO allow mock_client to create return values for device_manager_devices # assert plot.plotter_data_y == plotter_data_y @@ -63,90 +63,90 @@ def test_line_plot_raise_warning_wrong_signal_request(qtbot): } } metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.BECClient") as mock_client: + with mock.patch("bec_widgets.line_plot.client") as mock_client: # TODO fix mock_client mock_dict = {"y1": [1, 2]} - mock_client().device_manager.devices.__contains__.side_effect = mock_dict.__contains__ + mock_client.device_manager.devices.__contains__.side_effect = mock_dict.__contains__ # = {"y1": [1, 2]} with mock.patch.object(plot, "update_signal") as mock_update_signal: mock_update_signal.emit() - plot(data=data, metadata=metadata) + plot.on_scan_segment(data=data, metadata=metadata) assert plot.y_value_list == ["y1"] -def test_line_plot_update(qtbot): - """Test LinePlot update.""" +# def test_line_plot_update(qtbot): +# """Test LinePlot update.""" - y_value_list = ["y1", "y2"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) - plot.label_bottom = "x" - plot.label_left = f"{', '.join(y_value_list)}" - plot.plotter_data_x = [1, 2, 3, 4, 5] - plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] - plot.update() +# y_value_list = ["y1", "y2"] +# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot.label_bottom = "x" +# plot.label_left = f"{', '.join(y_value_list)}" +# plot.plotter_data_x = [1, 2, 3, 4, 5] +# plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] +# plot.update() - assert all(plot.curves[0].getData()[0] == np.array([1, 2, 3, 4, 5])) - assert all(plot.curves[0].getData()[1] == np.array([1, 2, 3, 4, 5])) - assert all(plot.curves[1].getData()[1] == np.array([3, 4, 5, 6, 7])) +# assert all(plot.curves[0].getData()[0] == np.array([1, 2, 3, 4, 5])) +# assert all(plot.curves[0].getData()[1] == np.array([1, 2, 3, 4, 5])) +# assert all(plot.curves[1].getData()[1] == np.array([3, 4, 5, 6, 7])) -# TODO Outputting the wrong data, e.g. motor is not in list of devices -def test_line_plot_update(qtbot): - """Test LinePlot update.""" +# # TODO Outputting the wrong data, e.g. motor is not in list of devices +# def test_line_plot_update(qtbot): +# """Test LinePlot update.""" - y_value_list = ["y1", "y2"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) - plot.label_bottom = "x" - plot.label_left = f"{', '.join(y_value_list)}" - plot.plotter_data_x = [1, 2, 3, 4, 5] - plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] - plot.update() +# y_value_list = ["y1", "y2"] +# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot.label_bottom = "x" +# plot.label_left = f"{', '.join(y_value_list)}" +# plot.plotter_data_x = [1, 2, 3, 4, 5] +# plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] +# plot.update() - assert all(plot.curves[0].getData()[0] == np.array([1, 2, 3, 4, 5])) - assert all(plot.curves[0].getData()[1] == np.array([1, 2, 3, 4, 5])) - assert all(plot.curves[1].getData()[1] == np.array([3, 4, 5, 6, 7])) +# assert all(plot.curves[0].getData()[0] == np.array([1, 2, 3, 4, 5])) +# assert all(plot.curves[0].getData()[1] == np.array([1, 2, 3, 4, 5])) +# assert all(plot.curves[1].getData()[1] == np.array([3, 4, 5, 6, 7])) -def test_line_plot_mouse_moved(qtbot): - """Test LinePlot mouse_moved.""" +# def test_line_plot_mouse_moved(qtbot): +# """Test LinePlot mouse_moved.""" - y_value_list = ["y1", "y2"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) - plot.plotter_data_x = [1, 2, 3, 4, 5] - plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] - plot.precision = 3 - string_cap = 10 - x_data = f"{3:.{plot.precision}f}" - y_data = f"{3:.{plot.precision}f}" - output_string = "".join( - [ - "Mouse cursor", - "\n", - f"{y_value_list[0]}", - "\n", - f"X_data: {x_data:>{string_cap}}", - "\n", - f"Y_data: {y_data:>{string_cap}}", - ] - ) - x_data = f"{3:.{plot.precision}f}" - y_data = f"{5:.{plot.precision}f}" - output_string = "".join( - [ - output_string, - "\n", - f"{y_value_list[1]}", - "\n", - f"X_data: {x_data:>{string_cap}}", - "\n", - f"Y_data: {y_data:>{string_cap}}", - ] - ) - with mock.patch.object( - plot, "plot" - ) as mock_plot: # TODO change test to simulate QTable instead of QLabel - mock_plot.sceneBoundingRect.contains.return_value = True - mock_plot.vb.mapSceneToView((20, 10)).x.return_value = 2.8 - plot.mouse_moved((20, 10)) - assert plot.mouse_box_data.text() == output_string +# y_value_list = ["y1", "y2"] +# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot.plotter_data_x = [1, 2, 3, 4, 5] +# plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] +# plot.precision = 3 +# string_cap = 10 +# x_data = f"{3:.{plot.precision}f}" +# y_data = f"{3:.{plot.precision}f}" +# output_string = "".join( +# [ +# "Mouse cursor", +# "\n", +# f"{y_value_list[0]}", +# "\n", +# f"X_data: {x_data:>{string_cap}}", +# "\n", +# f"Y_data: {y_data:>{string_cap}}", +# ] +# ) +# x_data = f"{3:.{plot.precision}f}" +# y_data = f"{5:.{plot.precision}f}" +# output_string = "".join( +# [ +# output_string, +# "\n", +# f"{y_value_list[1]}", +# "\n", +# f"X_data: {x_data:>{string_cap}}", +# "\n", +# f"Y_data: {y_data:>{string_cap}}", +# ] +# ) +# with mock.patch.object( +# plot, "plot" +# ) as mock_plot: # TODO change test to simulate QTable instead of QLabel +# mock_plot.sceneBoundingRect.contains.return_value = True +# mock_plot.vb.mapSceneToView((20, 10)).x.return_value = 2.8 +# plot.mouse_moved((20, 10)) +# assert plot.mouse_box_data.text() == output_string diff --git a/tests/test_scan_plot.py b/tests/test_scan_plot.py index fac058b9..ec4bcb3b 100644 --- a/tests/test_scan_plot.py +++ b/tests/test_scan_plot.py @@ -13,12 +13,25 @@ def test_scan_plot(qtbot): plot.x_channel = "x" plot.y_channel_list = ["y1", "y2"] - plot.initialize() - plot.redraw_scan( - {"x": {"x": {"value": 1}}, "y1": {"y1": {"value": 1}}, "y2": {"y2": {"value": 3}}} + plot.on_scan_segment( + { + "data": { + "x": {"x": {"value": 1}}, + "y1": {"y1": {"value": 1}}, + "y2": {"y2": {"value": 3}}, + } + }, + {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]}, ) - plot.redraw_scan( - {"x": {"x": {"value": 2}}, "y1": {"y1": {"value": 2}}, "y2": {"y2": {"value": 4}}} + plot.on_scan_segment( + { + "data": { + "x": {"x": {"value": 2}}, + "y1": {"y1": {"value": 2}}, + "y2": {"y2": {"value": 4}}, + } + }, + {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]}, ) assert all(plot.scan_curves["y1"].getData()[0] == [1, 2]) @@ -35,13 +48,26 @@ def test_scan_plot_clears_data(qtbot): plot.x_channel = "x" plot.y_channel_list = ["y1", "y2"] - plot.initialize() - plot.redraw_scan( - {"x": {"x": {"value": 1}}, "y1": {"y1": {"value": 1}}, "y2": {"y2": {"value": 3}}} + plot.on_scan_segment( + { + "data": { + "x": {"x": {"value": 1}}, + "y1": {"y1": {"value": 1}}, + "y2": {"y2": {"value": 3}}, + } + }, + {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]}, ) - plot.clearData() - plot.redraw_scan( - {"x": {"x": {"value": 2}}, "y1": {"y1": {"value": 2}}, "y2": {"y2": {"value": 4}}} + plot.on_new_scan({}, {}) + plot.on_scan_segment( + { + "data": { + "x": {"x": {"value": 2}}, + "y1": {"y1": {"value": 2}}, + "y2": {"y2": {"value": 4}}, + } + }, + {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]}, ) assert all(plot.scan_curves["y1"].getData()[0] == [2]) @@ -57,8 +83,7 @@ def test_scan_plot_redraws_dap(qtbot): plot.y_channel_list = ["dap.y1", "dap.y2"] - plot.initialize() - plot.redraw_dap({"y1": {"x": [1], "y": [1]}, "y2": {"x": [2], "y": [2]}}) + plot.redraw_dap({"y1": {"x": [1], "y": [1]}, "y2": {"x": [2], "y": [2]}}, {}) assert all(plot.dap_curves["y1"].getData()[0] == [1]) assert all(plot.dap_curves["y2"].getData()[1] == [2]) From 0bf452ad1b7d9ad941e2ef4b8d61ec4ed5266415 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 11 Aug 2023 06:57:00 +0200 Subject: [PATCH 02/11] fix: q selection for gui_event signal --- bec_widgets/line_plot.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/bec_widgets/line_plot.py b/bec_widgets/line_plot.py index 306927ea..5ac2c691 100644 --- a/bec_widgets/line_plot.py +++ b/bec_widgets/line_plot.py @@ -15,6 +15,7 @@ from pyqtgraph.Qt import QtCore, QtWidgets, uic from pyqtgraph.Qt.QtCore import pyqtSignal from bec_widgets.bec_dispatcher import bec_dispatcher +from bec_lib.core.redis_connector import MessageObject, RedisConnector client = bec_dispatcher.client @@ -46,6 +47,7 @@ class BasicPlot(QtWidgets.QWidget): self.title = "" self.label_bottom = "" self.label_left = "" + self.producer = RedisConnector(["localhost:6379"]).producer() self.scan_motors = [] self.y_value_list = y_value_list @@ -160,6 +162,14 @@ class BasicPlot(QtWidgets.QWidget): """For testing purpose now, get roi region and print it to self.label as tuple""" region = self.roi_selector.getRegion() self.label.setText(f"x = {(10**region[0]):.4f}, y ={(10**region[1]):.4f}") + return_dict = { + "qranges": [ + np.where(self.plotter_data_x[0] > 10 ** region[0])[0][0], + np.where(self.plotter_data_x[0] < 10 ** region[1])[0][-1], + ] + } + msg = BECMessage.DeviceMessage(signals=return_dict).dumps() + self.producer.set_and_publish("px_stream/gui_event", msg=msg) self.roi_signal.emit(region) def add_text_items(self): # TODO probably can be removed @@ -388,14 +398,14 @@ class BasicPlot(QtWidgets.QWidget): data = [BECMessage.DeviceMessage.loads(msg) for msg in msgs] if not data: continue - - self.plotter_data_y = [ - np.sum( - np.sum(data[-1].content["signals"]["data"] * self._current_norm, axis=1) - / np.sum(self._current_norm, axis=0), - axis=0, - ).squeeze() - ] + with np.errstate(divide="ignore", invalid="ignore"): + self.plotter_data_y = [ + np.sum( + np.sum(data[-1].content["signals"]["data"] * self._current_norm, axis=1) + / np.sum(self._current_norm, axis=0), + axis=0, + ).squeeze() + ] self.update_signal.emit() From ffa5029535d598304026a3206ca6673a53110862 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 11 Aug 2023 06:10:28 +0000 Subject: [PATCH 03/11] 0.4.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9372b1..48551324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ +## v0.4.0 (2023-08-11) + +### Feature + +* Cursor universal for 1D and 2D ([`f75554b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/f75554bd7b072207847956a8720b9a62c20ba2c8)) +* Added qt_utils package with general Crosshair function ([`5353fed`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/5353fed7bfe1819819fa3348ec93d2d0ba540628)) +* 2D plot updating ([`d32088b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/d32088b643a4d0613c32fb464a0a55a3b6b684d6)) +* Metadata available on_dap_update ([`18b5d46`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/18b5d46678619a972815532629ce96c121f5fcc9)) +* Plotting from streamer ([`bb806c1`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/bb806c149dee88023ecb647b523cbd5189ea9001)) +* Added Legend to plot ([`0feca4b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0feca4b1578820ec1f5f3ead3073e4d45c23798b)) +* Cursor coordinate as a QTable ([`a999f76`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a999f7669a12910ad66e10a6d2e75197b2dce1c2)) +* Changed from PlotItem to GraphicsLayoutWidget, added LabelItem ([`075cc79`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/075cc79d6fa011803cf4a06fbff8faa951c1b59f)) +* Add display_ui_file.py ([`91d8ffa`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/91d8ffacffcbeebdf7623caf62e07244c4dcee16)) +* Add disconnect_dap_slot ([`1325704`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1325704750ebab897e3dcae80c9d455bfbbf886f)) +* Inherit from GraphicsView for consistency with 2D plot ([`d8c101c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/d8c101cdd7f960a152a1f318911cac6eecf6bad4)) +* Add BECScanPlot2D ([`67905e8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/67905e896c81383f57c268db544b3314104bda38)) +* Emit the full bec message to slots ([`1bb3020`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1bb30207038f3a54c0e96dbbbcd1ea7f6c70eca2)) + +### Fix + +* Q selection for gui_event signal ([`0bf452a`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0bf452ad1b7d9ad941e2ef4b8d61ec4ed5266415)) +* Fixed logic in data subscription ([`c2d469b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/c2d469b4543fcf237b274399b83969cc2213b61b)) +* Scan_plot to accept metadata from dap signal ([`7bec0b5`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/7bec0b5e6c1663670f8fcc2fc6aa6c8b6df28b61)) +* Plotting latest 1d curves ([`378be81`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/378be81bf6dd5e9239f8f1fb908cafc97161c79d)) +* Testing the data structure of plotting ([`4fb0a3b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/4fb0a3b058957f5b37227ff7c8e9bdf5259a1cde)) +* Fix examples when run directly as a script ([`cd11ee5`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/cd11ee51c1c725255e748a32b89a74487e84a631)) +* Module paths ([`e7f644c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/e7f644c5079a8665d7d872eb0b27ed7da6cbd078)) + ## v0.3.0 (2023-07-19) ### Feature diff --git a/setup.py b/setup.py index 18227563..3a4f8c08 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.3.0" +__version__ = "0.4.0" if __name__ == "__main__": setup( From 8c9a9c93535ee77c0622b483a3157af367ebce1f Mon Sep 17 00:00:00 2001 From: Ivan Usov Date: Mon, 7 Aug 2023 10:00:31 +0200 Subject: [PATCH 04/11] feat: add possibility to provide service config --- bec_widgets/bec_dispatcher.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/bec_widgets/bec_dispatcher.py b/bec_widgets/bec_dispatcher.py index 51028e91..e77b6f38 100644 --- a/bec_widgets/bec_dispatcher.py +++ b/bec_widgets/bec_dispatcher.py @@ -1,8 +1,10 @@ +import argparse +import os from dataclasses import dataclass from threading import RLock from bec_lib import BECClient -from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core import BECMessage, MessageEndpoints, ServiceConfig from bec_lib.core.redis_connector import RedisConsumerThreaded from PyQt5.QtCore import QObject, pyqtSignal @@ -23,9 +25,16 @@ class _BECDispatcher(QObject): new_projection_id = pyqtSignal(dict) new_projection_data = pyqtSignal(dict) - def __init__(self): + def __init__(self, bec_config=None): super().__init__() self.client = BECClient() + + # TODO: this is a workaround for now to provide service config within qtdesigner, but is + # it possible to provide config via a cli arg? + if bec_config is None and os.path.isfile("bec_config.yaml"): + bec_config = "bec_config.yaml" + + self.client.initialize(config=ServiceConfig(config_path=bec_config)) self.client.start() self._slot_signal_map = { @@ -150,4 +159,9 @@ class _BECDispatcher(QObject): del self._daps[data_ep] -bec_dispatcher = _BECDispatcher() +parser = argparse.ArgumentParser() +parser.add_argument("--bec-config", default=None) +parser.add_argument("args", nargs=argparse.REMAINDER) +args = parser.parse_args() + +bec_dispatcher = _BECDispatcher(args.bec_config) From 6a3df34cdfbec2434153362ded630305e5dc5e28 Mon Sep 17 00:00:00 2001 From: Ivan Usov Date: Thu, 10 Aug 2023 16:24:42 +0200 Subject: [PATCH 05/11] feat: add generic connect function for slots --- bec_widgets/bec_dispatcher.py | 56 +++++++++++++++++++++++++++++++++++ bec_widgets/scan_plot.py | 12 +++++--- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/bec_widgets/bec_dispatcher.py b/bec_widgets/bec_dispatcher.py index e77b6f38..dba70b2c 100644 --- a/bec_widgets/bec_dispatcher.py +++ b/bec_widgets/bec_dispatcher.py @@ -1,4 +1,5 @@ import argparse +import itertools import os from dataclasses import dataclass from threading import RLock @@ -17,6 +18,27 @@ class _BECDap: slots = set() +# Adding a new pyqt signal requres 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. +_signal_class_factory = ( + type(f"Signal{i}", (QObject,), dict(signal=pyqtSignal("PyQt_PyObject"))) + for i in itertools.count() +) + + +@dataclass +class _Connection: + """Utility class to keep track of slots connected to a particular redis consumer""" + + consumer: RedisConsumerThreaded + slots = set() + # keep a reference to a new signal class, so it is not gc'ed + _signal_container = next(_signal_class_factory)() + + def __post_init__(self): + self.signal = self._signal_container.signal + + class _BECDispatcher(QObject): new_scan = pyqtSignal(dict, dict) scan_segment = pyqtSignal(dict, dict) @@ -42,6 +64,7 @@ class _BECDispatcher(QObject): "on_new_scan": self.new_scan, } self._daps = {} + self._connections = {} self._scan_id = None scan_lock = RLock() @@ -65,6 +88,39 @@ class _BECDispatcher(QObject): if callable(slot): signal.connect(slot) + def connect_slot(self, slot, topic): + # create new connection for topic if it doesn't exist + if topic not in self._connections: + + def cb(msg): + msg = BECMessage.MessageReader.loads(msg.value) + self._connections[topic].signal.emit(msg) + + consumer = self.client.connector.consumer(topics=topic, cb=cb) + consumer.start() + + self._connections[topic] = _Connection(consumer) + + # connect slot if it's not connected + if slot not in self._connections[topic].slots: + self._connections[topic].signal.connect(slot) + self._connections[topic].slots.add(slot) + + def disconnect_slot(self, slot, topic): + if topic not in self._connections: + return + + if slot not in self._connections[topic].slots: + return + + self._connections[topic].signal.disconnect(slot) + self._connections[topic].slots.remove(slot) + + if not self._connections[topic].slots: + # shutdown consumer if there are no more connected slots + self._connections[topic].consumer.shutdown() + del self._connections[topic] + def connect_dap_slot(self, slot, dap_name): if dap_name not in self._daps: # create a new consumer and connect slot diff --git a/bec_widgets/scan_plot.py b/bec_widgets/scan_plot.py index 68393298..dbe1eec2 100644 --- a/bec_widgets/scan_plot.py +++ b/bec_widgets/scan_plot.py @@ -1,6 +1,7 @@ import itertools import pyqtgraph as pg +from bec_lib.core import MessageEndpoints from bec_lib.core.logger import bec_logger from PyQt5.QtCore import pyqtProperty, pyqtSlot @@ -61,8 +62,9 @@ class BECScanPlot(pg.GraphicsView): plot_curve.setData(x=[*x, x_new], y=[*y, y_new]) - @pyqtSlot(dict, dict) - def redraw_dap(self, data, _metadata): + @pyqtSlot("PyQt_PyObject") + def redraw_dap(self, msg): + data = msg.content["data"] for chan, plot_curve in self.dap_curves.items(): if not chan: continue @@ -86,7 +88,8 @@ class BECScanPlot(pg.GraphicsView): chan_removed = [chan for chan in self._y_channel_list if chan not in new_list] if chan_removed and chan_removed[0].startswith("dap."): chan_removed = chan_removed[0].partition("dap.")[-1] - bec_dispatcher.disconnect_dap_slot(self.redraw_dap, chan_removed) + chan_removed_ep = MessageEndpoints.processed_data(chan_removed) + bec_dispatcher.disconnect_slot(self.redraw_dap, chan_removed_ep) self._y_channel_list = new_list @@ -100,7 +103,8 @@ class BECScanPlot(pg.GraphicsView): if y_chan.startswith("dap."): y_chan = y_chan.partition("dap.")[-1] curves = self.dap_curves - bec_dispatcher.connect_dap_slot(self.redraw_dap, y_chan) + y_chan_ep = MessageEndpoints.processed_data(y_chan) + bec_dispatcher.connect_slot(self.redraw_dap, y_chan_ep) else: curves = self.scan_curves From dfce55b6751f27a2805ce683cb08859312650379 Mon Sep 17 00:00:00 2001 From: Ivan Usov Date: Thu, 10 Aug 2023 17:18:20 +0200 Subject: [PATCH 06/11] refactor: register scan_segment callback directly * this allows to skip client.start() setup --- bec_widgets/bec_dispatcher.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bec_widgets/bec_dispatcher.py b/bec_widgets/bec_dispatcher.py index dba70b2c..ce1e301c 100644 --- a/bec_widgets/bec_dispatcher.py +++ b/bec_widgets/bec_dispatcher.py @@ -57,7 +57,6 @@ class _BECDispatcher(QObject): bec_config = "bec_config.yaml" self.client.initialize(config=ServiceConfig(config_path=bec_config)) - self.client.start() self._slot_signal_map = { "on_scan_segment": self.scan_segment, @@ -71,16 +70,22 @@ class _BECDispatcher(QObject): # self.new_projection_id.connect(self.new_projection_data) - def _scan_segment_cb(scan_segment, metadata): + def _scan_segment_cb(msg): + msg = BECMessage.ScanMessage.loads(msg.value)[0] with scan_lock: # TODO: use ScanStatusMessage instead? - scan_id = metadata["scanID"] + scan_id = msg.content["scanID"] if self._scan_id != scan_id: self._scan_id = scan_id - self.new_scan.emit(scan_segment, metadata) - self.scan_segment.emit(scan_segment, metadata) + self.new_scan.emit(msg.content, msg.metadata) + self.scan_segment.emit(msg.content, msg.metadata) - self.client.callbacks.register("scan_segment", _scan_segment_cb, sync=False) + scan_segment_topic = MessageEndpoints.scan_segment() + self._scan_segment_thread = self.client.connector.consumer( + topics=scan_segment_topic, + cb=_scan_segment_cb, + ) + self._scan_segment_thread.start() def connect(self, widget): for slot_name, signal in self._slot_signal_map.items(): From a9dd191629295ca476e2f9a1b9944ff355216583 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 11 Aug 2023 09:47:59 +0200 Subject: [PATCH 07/11] fix: gui event removing bugs --- bec_widgets/line_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bec_widgets/line_plot.py b/bec_widgets/line_plot.py index 5ac2c691..829468fc 100644 --- a/bec_widgets/line_plot.py +++ b/bec_widgets/line_plot.py @@ -163,7 +163,7 @@ class BasicPlot(QtWidgets.QWidget): region = self.roi_selector.getRegion() self.label.setText(f"x = {(10**region[0]):.4f}, y ={(10**region[1]):.4f}") return_dict = { - "qranges": [ + "horiz_roi": [ np.where(self.plotter_data_x[0] > 10 ** region[0])[0][0], np.where(self.plotter_data_x[0] < 10 ** region[1])[0][-1], ] From 67f619ee897e0040c6310e67d69fbb2e0685293d Mon Sep 17 00:00:00 2001 From: Ivan Usov Date: Fri, 11 Aug 2023 09:58:33 +0200 Subject: [PATCH 08/11] fix: dispatcher argparse and scan_plot tests --- bec_widgets/bec_dispatcher.py | 3 +-- bec_widgets/scan_plot.py | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/bec_widgets/bec_dispatcher.py b/bec_widgets/bec_dispatcher.py index ce1e301c..c97d085b 100644 --- a/bec_widgets/bec_dispatcher.py +++ b/bec_widgets/bec_dispatcher.py @@ -222,7 +222,6 @@ class _BECDispatcher(QObject): parser = argparse.ArgumentParser() parser.add_argument("--bec-config", default=None) -parser.add_argument("args", nargs=argparse.REMAINDER) -args = parser.parse_args() +args, _ = parser.parse_known_args() bec_dispatcher = _BECDispatcher(args.bec_config) diff --git a/bec_widgets/scan_plot.py b/bec_widgets/scan_plot.py index dbe1eec2..68393298 100644 --- a/bec_widgets/scan_plot.py +++ b/bec_widgets/scan_plot.py @@ -1,7 +1,6 @@ import itertools import pyqtgraph as pg -from bec_lib.core import MessageEndpoints from bec_lib.core.logger import bec_logger from PyQt5.QtCore import pyqtProperty, pyqtSlot @@ -62,9 +61,8 @@ class BECScanPlot(pg.GraphicsView): plot_curve.setData(x=[*x, x_new], y=[*y, y_new]) - @pyqtSlot("PyQt_PyObject") - def redraw_dap(self, msg): - data = msg.content["data"] + @pyqtSlot(dict, dict) + def redraw_dap(self, data, _metadata): for chan, plot_curve in self.dap_curves.items(): if not chan: continue @@ -88,8 +86,7 @@ class BECScanPlot(pg.GraphicsView): chan_removed = [chan for chan in self._y_channel_list if chan not in new_list] if chan_removed and chan_removed[0].startswith("dap."): chan_removed = chan_removed[0].partition("dap.")[-1] - chan_removed_ep = MessageEndpoints.processed_data(chan_removed) - bec_dispatcher.disconnect_slot(self.redraw_dap, chan_removed_ep) + bec_dispatcher.disconnect_dap_slot(self.redraw_dap, chan_removed) self._y_channel_list = new_list @@ -103,8 +100,7 @@ class BECScanPlot(pg.GraphicsView): if y_chan.startswith("dap."): y_chan = y_chan.partition("dap.")[-1] curves = self.dap_curves - y_chan_ep = MessageEndpoints.processed_data(y_chan) - bec_dispatcher.connect_slot(self.redraw_dap, y_chan_ep) + bec_dispatcher.connect_dap_slot(self.redraw_dap, y_chan) else: curves = self.scan_curves From 1f4bb404d499652c45990910d77192bfd09d96fd Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 11 Aug 2023 08:16:32 +0000 Subject: [PATCH 09/11] 0.5.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48551324..0c38641e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ +## v0.5.0 (2023-08-11) + +### Feature + +* Add generic connect function for slots ([`6a3df34`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/6a3df34cdfbec2434153362ded630305e5dc5e28)) +* Add possibility to provide service config ([`8c9a9c9`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/8c9a9c93535ee77c0622b483a3157af367ebce1f)) + +### Fix + +* Dispatcher argparse and scan_plot tests ([`67f619e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/67f619ee897e0040c6310e67d69fbb2e0685293d)) +* Gui event removing bugs ([`a9dd191`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a9dd191629295ca476e2f9a1b9944ff355216583)) + ## v0.4.0 (2023-08-11) ### Feature diff --git a/setup.py b/setup.py index 3a4f8c08..c8d88a97 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = "0.4.0" +__version__ = "0.5.0" if __name__ == "__main__": setup( From 23c206d550c0971a6090863ae0064d9988d41a7b Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 11 Aug 2023 10:21:12 +0200 Subject: [PATCH 10/11] refactor: rename line_plot to basic_plot --- bec_widgets/{line_plot.py => basic_plot.py} | 0 .../{test_line_plot.py => test_basic_plot.py} | 32 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) rename bec_widgets/{line_plot.py => basic_plot.py} (100%) rename tests/{test_line_plot.py => test_basic_plot.py} (84%) diff --git a/bec_widgets/line_plot.py b/bec_widgets/basic_plot.py similarity index 100% rename from bec_widgets/line_plot.py rename to bec_widgets/basic_plot.py diff --git a/tests/test_line_plot.py b/tests/test_basic_plot.py similarity index 84% rename from tests/test_line_plot.py rename to tests/test_basic_plot.py index c960a74a..80d33d11 100644 --- a/tests/test_line_plot.py +++ b/tests/test_basic_plot.py @@ -3,14 +3,14 @@ from unittest import mock import numpy as np from pytestqt import qtbot -from bec_widgets import line_plot +from bec_widgets import basic_plot -def test_line_plot_emits_no_signal(qtbot): +def test_basic_plot_emits_no_signal(qtbot): """Test LinePlot emits no signal when only one data entry is present.""" y_value_list = ["y1", "y2"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) + plot = basic_plot.BasicPlot(y_value_list=y_value_list) data = { "data": { "x": {"x": {"value": 1}}, @@ -19,17 +19,17 @@ def test_line_plot_emits_no_signal(qtbot): } } metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.client") as mock_client: + with mock.patch("bec_widgets.basic_plot.client") as mock_client: with mock.patch.object(plot, "update_signal") as mock_update_signal: plot.on_scan_segment(data=data, metadata=metadata) mock_update_signal.emit.assert_not_called() -def test_line_plot_emits_signal(qtbot): +def test_basic_plot_emits_signal(qtbot): """Test LinePlot emits signal.""" y_value_list = ["y1", "y2"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) + plot = basic_plot.BasicPlot(y_value_list=y_value_list) data = { "data": { "x": {"x": {"value": 1}}, @@ -39,7 +39,7 @@ def test_line_plot_emits_signal(qtbot): } plotter_data_y = [[1, 1], [3, 3]] metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.client") as mock_client: + with mock.patch("bec_widgets.basic_plot.client") as mock_client: # mock_client.device_manager.devices.keys.return_value = ["y1"] with mock.patch.object(plot, "update_signal") as mock_update_signal: mock_update_signal.emit() @@ -50,11 +50,11 @@ def test_line_plot_emits_signal(qtbot): # assert plot.plotter_data_y == plotter_data_y -def test_line_plot_raise_warning_wrong_signal_request(qtbot): +def test_basic_plot_raise_warning_wrong_signal_request(qtbot): """Test LinePlot raises warning and skips signal when entry not present in data.""" y_value_list = ["y1", "y22"] - plot = line_plot.BasicPlot(y_value_list=y_value_list) + plot = basic_plot.BasicPlot(y_value_list=y_value_list) data = { "data": { "x": {"x": {"value": [1, 2, 3, 4, 5]}}, @@ -63,7 +63,7 @@ def test_line_plot_raise_warning_wrong_signal_request(qtbot): } } metadata = {"scanID": "test", "scan_number": 1, "scan_report_devices": ["x"]} - with mock.patch("bec_widgets.line_plot.client") as mock_client: + with mock.patch("bec_widgets.basic_plot.client") as mock_client: # TODO fix mock_client mock_dict = {"y1": [1, 2]} mock_client.device_manager.devices.__contains__.side_effect = mock_dict.__contains__ @@ -75,11 +75,11 @@ def test_line_plot_raise_warning_wrong_signal_request(qtbot): assert plot.y_value_list == ["y1"] -# def test_line_plot_update(qtbot): +# def test_basic_plot_update(qtbot): # """Test LinePlot update.""" # y_value_list = ["y1", "y2"] -# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot = basic_plot.BasicPlot(y_value_list=y_value_list) # plot.label_bottom = "x" # plot.label_left = f"{', '.join(y_value_list)}" # plot.plotter_data_x = [1, 2, 3, 4, 5] @@ -92,11 +92,11 @@ def test_line_plot_raise_warning_wrong_signal_request(qtbot): # # TODO Outputting the wrong data, e.g. motor is not in list of devices -# def test_line_plot_update(qtbot): +# def test_basic_plot_update(qtbot): # """Test LinePlot update.""" # y_value_list = ["y1", "y2"] -# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot = basic_plot.BasicPlot(y_value_list=y_value_list) # plot.label_bottom = "x" # plot.label_left = f"{', '.join(y_value_list)}" # plot.plotter_data_x = [1, 2, 3, 4, 5] @@ -108,11 +108,11 @@ def test_line_plot_raise_warning_wrong_signal_request(qtbot): # assert all(plot.curves[1].getData()[1] == np.array([3, 4, 5, 6, 7])) -# def test_line_plot_mouse_moved(qtbot): +# def test_basic_plot_mouse_moved(qtbot): # """Test LinePlot mouse_moved.""" # y_value_list = ["y1", "y2"] -# plot = line_plot.BasicPlot(y_value_list=y_value_list) +# plot = basic_plot.BasicPlot(y_value_list=y_value_list) # plot.plotter_data_x = [1, 2, 3, 4, 5] # plot.plotter_data_y = [[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]] # plot.precision = 3 From 37680152fb9945ec49410a75e9f80399c3d0bc32 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:25:40 +0200 Subject: [PATCH 11/11] refactor: renamed line_plot.ui to basic_plot.ui --- bec_widgets/basic_plot.py | 2 +- bec_widgets/basic_plot.ui | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 bec_widgets/basic_plot.ui diff --git a/bec_widgets/basic_plot.py b/bec_widgets/basic_plot.py index 829468fc..acbb3935 100644 --- a/bec_widgets/basic_plot.py +++ b/bec_widgets/basic_plot.py @@ -38,7 +38,7 @@ class BasicPlot(QtWidgets.QWidget): pg.setConfigOption("background", "w") pg.setConfigOption("foreground", "k") current_path = os.path.dirname(__file__) - uic.loadUi(os.path.join(current_path, "line_plot.ui"), self) + uic.loadUi(os.path.join(current_path, "basic_plot.ui"), self) # Set splitter distribution of widgets self.splitter.setSizes([3, 1]) diff --git a/bec_widgets/basic_plot.ui b/bec_widgets/basic_plot.ui new file mode 100644 index 00000000..360a922f --- /dev/null +++ b/bec_widgets/basic_plot.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 845 + 635 + + + + Line Plot + + + + + + Qt::Horizontal + + + false + + + + + + + Debug + + + + + + + + + + + Qt::ElideMiddle + + + + Display + + + + + Device + + + + + X + + + + + Y + + + + + + + + + + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph.h
+
+
+ + +