bec-widgets 0.55.0__py3-none-any.whl → 0.56.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. .gitlab-ci.yml +113 -8
  2. CHANGELOG.md +34 -28
  3. PKG-INFO +3 -1
  4. bec_widgets/examples/jupyter_console/jupyter_console_window.py +28 -38
  5. bec_widgets/examples/motor_movement/motor_control_compilations.py +1 -7
  6. bec_widgets/utils/__init__.py +1 -0
  7. bec_widgets/utils/crosshair.py +13 -9
  8. bec_widgets/utils/ui_loader.py +58 -0
  9. bec_widgets/widgets/motor_control/motor_table/motor_table.py +44 -43
  10. bec_widgets/widgets/motor_control/movement_absolute/movement_absolute.py +25 -23
  11. bec_widgets/widgets/motor_control/movement_relative/movement_relative.py +51 -48
  12. bec_widgets/widgets/spiral_progress_bar/ring.py +5 -5
  13. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/METADATA +3 -1
  14. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/RECORD +22 -43
  15. docs/user/apps.md +1 -26
  16. pyproject.toml +2 -1
  17. tests/end-2-end/test_bec_dock_rpc_e2e.py +1 -1
  18. tests/unit_tests/test_client_utils.py +2 -2
  19. tests/unit_tests/test_crosshair.py +5 -5
  20. tests/unit_tests/test_motor_control.py +49 -45
  21. bec_widgets/examples/eiger_plot/__init__.py +0 -0
  22. bec_widgets/examples/eiger_plot/eiger_plot.py +0 -307
  23. bec_widgets/examples/eiger_plot/eiger_plot.ui +0 -207
  24. bec_widgets/examples/mca_readout/__init__.py +0 -0
  25. bec_widgets/examples/mca_readout/mca_plot.py +0 -159
  26. bec_widgets/examples/mca_readout/mca_sim.py +0 -28
  27. bec_widgets/examples/modular_app/___init__.py +0 -0
  28. bec_widgets/examples/modular_app/modular.ui +0 -92
  29. bec_widgets/examples/modular_app/modular_app.py +0 -197
  30. bec_widgets/examples/motor_movement/config_example.yaml +0 -17
  31. bec_widgets/examples/motor_movement/csax_bec_config.yaml +0 -10
  32. bec_widgets/examples/motor_movement/csaxs_config.yaml +0 -17
  33. bec_widgets/examples/motor_movement/motor_example.py +0 -1344
  34. bec_widgets/examples/stream_plot/__init__.py +0 -0
  35. bec_widgets/examples/stream_plot/line_plot.ui +0 -155
  36. bec_widgets/examples/stream_plot/stream_plot.py +0 -337
  37. docs/user/apps/modular_app.md +0 -6
  38. docs/user/apps/motor_app.md +0 -34
  39. docs/user/apps/motor_app_10fps.gif +0 -0
  40. docs/user/apps/plot_app.md +0 -6
  41. tests/unit_tests/test_eiger_plot.py +0 -115
  42. tests/unit_tests/test_stream_plot.py +0 -158
  43. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/WHEEL +0 -0
  44. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/licenses/LICENSE +0 -0
File without changes
@@ -1,155 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <ui version="4.0">
3
- <class>Form</class>
4
- <widget class="QWidget" name="Form">
5
- <property name="geometry">
6
- <rect>
7
- <x>0</x>
8
- <y>0</y>
9
- <width>845</width>
10
- <height>635</height>
11
- </rect>
12
- </property>
13
- <property name="windowTitle">
14
- <string>Line Plot</string>
15
- </property>
16
- <layout class="QGridLayout" name="gridLayout">
17
- <item row="0" column="0">
18
- <widget class="QSplitter" name="splitter">
19
- <property name="orientation">
20
- <enum>Qt::Horizontal</enum>
21
- </property>
22
- <widget class="QSplitter" name="splitter_plot">
23
- <property name="sizePolicy">
24
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
25
- <horstretch>1</horstretch>
26
- <verstretch>1</verstretch>
27
- </sizepolicy>
28
- </property>
29
- <property name="orientation">
30
- <enum>Qt::Vertical</enum>
31
- </property>
32
- <widget class="GraphicsLayoutWidget" name="glw_plot"/>
33
- <widget class="GraphicsLayoutWidget" name="glw_image"/>
34
- </widget>
35
- <widget class="QWidget" name="">
36
- <layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,1,15">
37
- <item>
38
- <widget class="QPushButton" name="pushButton_generate">
39
- <property name="text">
40
- <string>Generate 1D and 2D data without stream</string>
41
- </property>
42
- </widget>
43
- </item>
44
- <item>
45
- <widget class="QGroupBox" name="groupBox">
46
- <property name="sizePolicy">
47
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
48
- <horstretch>0</horstretch>
49
- <verstretch>0</verstretch>
50
- </sizepolicy>
51
- </property>
52
- <property name="title">
53
- <string>1st angle of azimutal segment (deg)</string>
54
- </property>
55
- <layout class="QHBoxLayout" name="horizontalLayout">
56
- <item>
57
- <widget class="QDoubleSpinBox" name="doubleSpinBox">
58
- <property name="sizePolicy">
59
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
60
- <horstretch>0</horstretch>
61
- <verstretch>0</verstretch>
62
- </sizepolicy>
63
- </property>
64
- <property name="maximum">
65
- <double>360.000000000000000</double>
66
- </property>
67
- <property name="singleStep">
68
- <double>0.250000000000000</double>
69
- </property>
70
- </widget>
71
- </item>
72
- <item>
73
- <widget class="QComboBox" name="comboBox">
74
- <property name="sizePolicy">
75
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
76
- <horstretch>0</horstretch>
77
- <verstretch>0</verstretch>
78
- </sizepolicy>
79
- </property>
80
- <item>
81
- <property name="text">
82
- <string>f1amp</string>
83
- </property>
84
- </item>
85
- <item>
86
- <property name="text">
87
- <string>f2amp</string>
88
- </property>
89
- </item>
90
- <item>
91
- <property name="text">
92
- <string>f2 phase</string>
93
- </property>
94
- </item>
95
- </widget>
96
- </item>
97
- </layout>
98
- </widget>
99
- </item>
100
- <item>
101
- <layout class="QHBoxLayout" name="horizontalLayout_2">
102
- <item>
103
- <widget class="QLabel" name="label">
104
- <property name="text">
105
- <string>Precision</string>
106
- </property>
107
- </widget>
108
- </item>
109
- <item>
110
- <widget class="QSpinBox" name="spinBox_precision">
111
- <property name="value">
112
- <number>4</number>
113
- </property>
114
- </widget>
115
- </item>
116
- </layout>
117
- </item>
118
- <item>
119
- <widget class="QTableWidget" name="cursor_table">
120
- <property name="textElideMode">
121
- <enum>Qt::ElideMiddle</enum>
122
- </property>
123
- <column>
124
- <property name="text">
125
- <string>Display</string>
126
- </property>
127
- </column>
128
- <column>
129
- <property name="text">
130
- <string>X</string>
131
- </property>
132
- </column>
133
- <column>
134
- <property name="text">
135
- <string>Y</string>
136
- </property>
137
- </column>
138
- </widget>
139
- </item>
140
- </layout>
141
- </widget>
142
- </widget>
143
- </item>
144
- </layout>
145
- </widget>
146
- <customwidgets>
147
- <customwidget>
148
- <class>GraphicsLayoutWidget</class>
149
- <extends>QGraphicsView</extends>
150
- <header>pyqtgraph.h</header>
151
- </customwidget>
152
- </customwidgets>
153
- <resources/>
154
- <connections/>
155
- </ui>
@@ -1,337 +0,0 @@
1
- import os
2
- import threading
3
- import time
4
-
5
- import numpy as np
6
- import pyqtgraph
7
- import pyqtgraph as pg
8
- from bec_lib import messages
9
- from bec_lib.endpoints import MessageEndpoints
10
- from bec_lib.redis_connector import RedisConnector
11
- from pyqtgraph import mkBrush, mkPen
12
- from pyqtgraph.Qt import QtCore, QtWidgets, uic
13
- from pyqtgraph.Qt.QtCore import pyqtSignal
14
- from qtpy.QtCore import Slot as pyqtSlot
15
- from qtpy.QtWidgets import QTableWidgetItem
16
-
17
- from bec_widgets.utils import Colors, Crosshair
18
- from bec_widgets.utils.bec_dispatcher import BECDispatcher
19
-
20
-
21
- class StreamPlot(QtWidgets.QWidget):
22
- update_signal = pyqtSignal()
23
- roi_signal = pyqtSignal(tuple)
24
-
25
- def __init__(self, name="", y_value_list=["gauss_bpm"], client=None, parent=None) -> None:
26
- """
27
- Basic plot widget for displaying scan data.
28
-
29
- Args:
30
- name (str, optional): Name of the plot. Defaults to "".
31
- y_value_list (list, optional): List of signals to be plotted. Defaults to ["gauss_bpm"].
32
- """
33
-
34
- # Client and device manager from BEC
35
- self.client = BECDispatcher().client if client is None else client
36
-
37
- super(StreamPlot, self).__init__()
38
- # Set style for pyqtgraph plots
39
- pg.setConfigOption("background", "w")
40
- pg.setConfigOption("foreground", "k")
41
- current_path = os.path.dirname(__file__)
42
- uic.loadUi(os.path.join(current_path, "line_plot.ui"), self)
43
-
44
- self._idle_time = 100
45
- self.connector = RedisConnector(["localhost:6379"])
46
-
47
- self.y_value_list = y_value_list
48
- self.previous_y_value_list = None
49
- self.plotter_data_x = []
50
- self.plotter_data_y = []
51
-
52
- self.plotter_scan_id = None
53
-
54
- self._current_proj = None
55
- self._current_metadata_ep = "px_stream/projection_{}/metadata"
56
-
57
- self.proxy_update = pg.SignalProxy(self.update_signal, rateLimit=25, slot=self.update)
58
-
59
- self._data_retriever_thread_exit_event = threading.Event()
60
- self.data_retriever = threading.Thread(
61
- target=self.on_projection, args=(self._data_retriever_thread_exit_event,), daemon=True
62
- )
63
- self.data_retriever.start()
64
-
65
- ##########################
66
- # UI
67
- ##########################
68
- self.init_ui()
69
- self.init_curves()
70
- self.hook_crosshair()
71
-
72
- def close(self):
73
- super().close()
74
- self._data_retriever_thread_exit_event.set()
75
- self.data_retriever.join()
76
-
77
- def init_ui(self):
78
- """Setup all ui elements"""
79
- ##########################
80
- # 1D Plot
81
- ##########################
82
-
83
- # LabelItem for ROI
84
- self.label_plot = pg.LabelItem(justify="center")
85
- self.glw_plot.addItem(self.label_plot)
86
- self.label_plot.setText("ROI region")
87
-
88
- # ROI selector - so far from [-1,1] #TODO update to scale with xrange
89
- self.roi_selector = pg.LinearRegionItem([-1, 1])
90
-
91
- self.glw_plot.nextRow() # TODO update of cursor
92
- self.label_plot_moved = pg.LabelItem(justify="center")
93
- self.glw_plot.addItem(self.label_plot_moved)
94
- self.label_plot_moved.setText("Actual coordinates (X, Y)")
95
-
96
- # Label for coordinates clicked
97
- self.glw_plot.nextRow()
98
- self.label_plot_clicked = pg.LabelItem(justify="center")
99
- self.glw_plot.addItem(self.label_plot_clicked)
100
- self.label_plot_clicked.setText("Clicked coordinates (X, Y)")
101
-
102
- # 1D PlotItem
103
- self.glw_plot.nextRow()
104
- self.plot = pg.PlotItem()
105
- self.plot.setLogMode(True, True)
106
- self.glw_plot.addItem(self.plot)
107
- self.plot.addLegend()
108
-
109
- ##########################
110
- # 2D Plot
111
- ##########################
112
-
113
- # Label for coordinates moved
114
- self.label_image_moved = pg.LabelItem(justify="center")
115
- self.glw_image.addItem(self.label_image_moved)
116
- self.label_image_moved.setText("Actual coordinates (X, Y)")
117
-
118
- # Label for coordinates clicked
119
- self.glw_image.nextRow()
120
- self.label_image_clicked = pg.LabelItem(justify="center")
121
- self.glw_image.addItem(self.label_image_clicked)
122
- self.label_image_clicked.setText("Clicked coordinates (X, Y)")
123
-
124
- # TODO try to lock aspect ratio with view
125
-
126
- # # Create a window
127
- # win = pg.GraphicsLayoutWidget()
128
- # win.show()
129
- #
130
- # # Create a ViewBox
131
- # view = win.addViewBox()
132
- #
133
- # # Lock the aspect ratio
134
- # view.setAspectLocked(True)
135
-
136
- # # Create an ImageItem
137
- # image_item = pg.ImageItem(np.random.random((100, 100)))
138
- #
139
- # # Add the ImageItem to the ViewBox
140
- # view.addItem(image_item)
141
-
142
- # 2D ImageItem
143
- self.glw_image.nextRow()
144
- self.plot_image = pg.PlotItem()
145
- self.glw_image.addItem(self.plot_image)
146
-
147
- def init_curves(self):
148
- # init of 1D plot
149
- self.plot.clear()
150
-
151
- self.curves = []
152
- self.pens = []
153
- self.brushs = []
154
-
155
- self.color_list = Colors.golden_angle_color(colormap="CET-R2", num=len(self.y_value_list))
156
-
157
- for ii, y_value in enumerate(self.y_value_list):
158
- pen = mkPen(color=self.color_list[ii], width=2, style=QtCore.Qt.DashLine)
159
- brush = mkBrush(color=self.color_list[ii])
160
- curve = pg.PlotDataItem(symbolBrush=brush, pen=pen, skipFiniteCheck=True, name=y_value)
161
- self.plot.addItem(curve)
162
- self.curves.append(curve)
163
- self.pens.append(pen)
164
- self.brushs.append(brush)
165
-
166
- # check if roi selector is in the plot
167
- if self.roi_selector not in self.plot.items:
168
- self.plot.addItem(self.roi_selector)
169
-
170
- # init of 2D plot
171
- self.plot_image.clear()
172
-
173
- self.img = pg.ImageItem()
174
- self.plot_image.addItem(self.img)
175
-
176
- # hooking signals
177
- self.hook_crosshair()
178
- self.init_table()
179
-
180
- def splitter_sizes(self): ...
181
-
182
- def hook_crosshair(self):
183
- self.crosshair_1d = Crosshair(self.plot, precision=4)
184
-
185
- self.crosshair_1d.coordinatesChanged1D.connect(
186
- lambda x, y: self.label_plot_moved.setText(f"Moved : ({x}, {y})")
187
- )
188
- self.crosshair_1d.coordinatesClicked1D.connect(
189
- lambda x, y: self.label_plot_clicked.setText(f"Moved : ({x}, {y})")
190
- )
191
-
192
- self.crosshair_1d.coordinatesChanged1D.connect(
193
- lambda x, y: self.update_table(table_widget=self.cursor_table, x=x, y_values=y)
194
- )
195
-
196
- self.crosshair_2D = Crosshair(self.plot_image)
197
-
198
- self.crosshair_2D.coordinatesChanged2D.connect(
199
- lambda x, y: self.label_image_moved.setText(f"Moved : ({x}, {y})")
200
- )
201
- self.crosshair_2D.coordinatesClicked2D.connect(
202
- lambda x, y: self.label_image_clicked.setText(f"Moved : ({x}, {y})")
203
- )
204
-
205
- # ROI
206
- self.roi_selector.sigRegionChangeFinished.connect(self.get_roi_region)
207
-
208
- def get_roi_region(self):
209
- """For testing purpose now, get roi region and print it to self.label as tuple"""
210
- region = self.roi_selector.getRegion()
211
- self.label_plot.setText(f"x = {(10 ** region[0]):.4f}, y ={(10 ** region[1]):.4f}")
212
- return_dict = {
213
- "horiz_roi": [
214
- np.where(self.plotter_data_x[0] > 10 ** region[0])[0][0],
215
- np.where(self.plotter_data_x[0] < 10 ** region[1])[0][-1],
216
- ]
217
- }
218
- msg = messages.DeviceMessage(signals=return_dict).dumps()
219
- self.connector.set_and_publish("px_stream/gui_event", msg=msg)
220
- self.roi_signal.emit(region)
221
-
222
- def init_table(self):
223
- # Init number of rows in table according to n of devices
224
- self.cursor_table.setRowCount(len(self.y_value_list))
225
- # self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"]) #TODO can be dynamic
226
- self.cursor_table.setVerticalHeaderLabels(self.y_value_list)
227
- self.cursor_table.resizeColumnsToContents()
228
-
229
- def update_table(self, table_widget, x, y_values):
230
- for i, y in enumerate(y_values):
231
- table_widget.setItem(i, 1, QTableWidgetItem(str(x)))
232
- table_widget.setItem(i, 2, QTableWidgetItem(str(y)))
233
- table_widget.resizeColumnsToContents()
234
-
235
- def update(self):
236
- """Update the plot with the new data."""
237
-
238
- # check if QTable was initialised and if list of devices was changed
239
- # if self.y_value_list != self.previous_y_value_list:
240
- # self.setup_cursor_table()
241
- # self.previous_y_value_list = self.y_value_list.copy() if self.y_value_list else None
242
-
243
- self.curves[0].setData(self.plotter_data_x[0], self.plotter_data_y[0])
244
-
245
- @staticmethod
246
- def flip_even_rows(arr):
247
- arr_copy = np.copy(arr) # Create a writable copy
248
- arr_copy[1::2, :] = arr_copy[1::2, ::-1]
249
- return arr_copy
250
-
251
- @staticmethod
252
- def remove_curve_by_name(plot: pyqtgraph.PlotItem, name: str) -> None:
253
- # def remove_curve_by_name(plot: pyqtgraph.PlotItem, checkbox: QtWidgets.QCheckBox, name: str) -> None:
254
- """Removes a curve from the given plot by the specified name.
255
-
256
- Args:
257
- plot (pyqtgraph.PlotItem): The plot from which to remove the curve.
258
- name (str): The name of the curve to remove.
259
- """
260
- # if checkbox.isChecked():
261
- for item in plot.items:
262
- if isinstance(item, pg.PlotDataItem) and getattr(item, "opts", {}).get("name") == name:
263
- plot.removeItem(item)
264
- return
265
-
266
- # else:
267
- # return
268
-
269
- def on_projection(self, exit_event):
270
- while not exit_event.is_set():
271
- if self._current_proj is None:
272
- time.sleep(0.1)
273
- continue
274
- endpoint = f"px_stream/projection_{self._current_proj}/data"
275
- msgs = self.client.connector.lrange(topic=endpoint, start=-1, end=-1)
276
- data = msgs
277
- if not data:
278
- continue
279
- with np.errstate(divide="ignore", invalid="ignore"):
280
- self.plotter_data_y = [
281
- np.sum(
282
- np.sum(data[-1].content["signals"]["data"] * self._current_norm, axis=1)
283
- / np.sum(self._current_norm, axis=0),
284
- axis=0,
285
- ).squeeze()
286
- ]
287
-
288
- self.update_signal.emit()
289
-
290
- @pyqtSlot(dict, dict)
291
- def on_dap_update(self, data: dict, metadata: dict):
292
- flipped_data = self.flip_even_rows(data["data"]["z"])
293
-
294
- self.img.setImage(flipped_data)
295
-
296
- @pyqtSlot(dict, dict)
297
- def new_proj(self, content: dict, _metadata: dict):
298
- proj_nr = content["signals"]["proj_nr"]
299
- endpoint = f"px_stream/projection_{proj_nr}/metadata"
300
- msg_raw = self.client.connector.get(topic=endpoint)
301
- msg = messages.DeviceMessage.loads(msg_raw)
302
- self._current_q = msg.content["signals"]["q"]
303
- self._current_norm = msg.content["signals"]["norm_sum"]
304
- self._current_metadata = msg.content["signals"]["metadata"]
305
-
306
- self.plotter_data_x = [self._current_q]
307
- self._current_proj = proj_nr
308
-
309
-
310
- if __name__ == "__main__":
311
- import argparse
312
-
313
- # from bec_widgets import ctrl_c # TODO uncomment when ctrl_c is ready to be compatible with qtpy
314
-
315
- parser = argparse.ArgumentParser()
316
- parser.add_argument(
317
- "--signals", help="specify recorded signals", nargs="+", default=["gauss_bpm"]
318
- )
319
- # default = ["gauss_bpm", "bpm4i", "bpm5i", "bpm6i", "xert"],
320
- value = parser.parse_args()
321
- print(f"Plotting signals for: {', '.join(value.signals)}")
322
-
323
- # Client from dispatcher
324
- bec_dispatcher = BECDispatcher()
325
- client = bec_dispatcher.client
326
-
327
- app = QtWidgets.QApplication([])
328
- # ctrl_c.setup(app) # TODO uncomment when ctrl_c is ready to be compatible with qtpy
329
- plot = StreamPlot(y_value_list=value.signals, client=client)
330
-
331
- bec_dispatcher.connect_slot(plot.new_proj, "px_stream/proj_nr")
332
- bec_dispatcher.connect_slot(
333
- plot.on_dap_update, MessageEndpoints.processed_data("px_dap_worker")
334
- )
335
- plot.show()
336
- # client.callbacks.register("scan_segment", plot, sync=False)
337
- app.exec()
@@ -1,6 +0,0 @@
1
- (user.apps.modular_app)=
2
-
3
- # Modular Application
4
-
5
-
6
- _to be added..._
@@ -1,34 +0,0 @@
1
- (user.apps.motor_app)=
2
- # Motor Alignment
3
-
4
- The Motor Alignment Application is a key component of the BEC Widgets suite, designed to facilitate precise alignment of motors.
5
- Users can easily launch this app using the script located at `/bec_widgets/example/motor_movement/motor_example.py` script.
6
- The application's primary function is to enable users to align motors to specific positions and to visually track the motor's trajectory.
7
-
8
- ## Controlling Motors
9
-
10
- In the top middle panel of the application, users will find combobox dropdown menus for selecting the motors they wish to track on the x and y axes of the motor map.
11
- These motors are automatically loaded from the current active BEC instance, ensuring seamless integration and ease of use.
12
-
13
- There are two primary methods to control motor movements:
14
-
15
-
16
- 1. **Manual Control with Arrow Keys:** Users can manually drive the motors using arrow keys. Before doing so, they need to select the step size for each motor, allowing for precise and incremental movements.
17
- 2. **Direct Position Entry:** Alternatively, users can input a desired position in the text input box and then click the Go button. This action will move the motor directly to the specified coordinates.
18
-
19
- As the motors are moved, their trajectory is plotted in real-time, providing users with a visual representation of the motor's path. This feature is particularly useful for understanding the movement patterns and making necessary adjustments.
20
-
21
-
22
- ## Saving and Exporting Data
23
-
24
- Users have the ability to save the current motor position in a table widget. This functionality is beneficial for recalling and returning to specific positions. By clicking the Go button in the table widget, the motors will automatically move back to the saved position.
25
-
26
- Additionally, users can annotate each saved position with notes and comments directly in the table widget. This feature is invaluable for keeping track of specific alignment settings or observations. The contents of the table, including the notes, can be exported to a .csv file. This exported data can be used for initiating scans or for record-keeping purposes.
27
-
28
- The table widget also supports saving and loading functionalities, allowing users to preserve their motor positions and notes across sessions. The saved files are in a user-friendly format for ease of access and use.
29
-
30
-
31
- ## Example of Use
32
-
33
- ![Motor app example](motor_app_10fps.gif)
34
-
Binary file
@@ -1,6 +0,0 @@
1
- (user.apps.plot_app)=
2
-
3
- # General Plotting Tool
4
-
5
-
6
- _to be added..._
@@ -1,115 +0,0 @@
1
- # pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
2
- import json
3
- from unittest.mock import MagicMock, patch
4
-
5
- import numpy as np
6
- import pyqtgraph as pg
7
- import pytest
8
- import zmq
9
-
10
- from bec_widgets.examples.eiger_plot.eiger_plot import EigerPlot
11
-
12
-
13
- # Common fixture for all tests
14
- @pytest.fixture
15
- def eiger_plot_instance(qtbot):
16
- widget = EigerPlot()
17
- qtbot.addWidget(widget)
18
- qtbot.waitExposed(widget)
19
- yield widget
20
- widget.close()
21
-
22
-
23
- @pytest.mark.parametrize(
24
- "fft_checked, rotation_index, transpose_checked, log_checked, expected_image",
25
- [
26
- (False, 0, False, False, np.array([[2, 1], [1, 5]], dtype=float)), # just mask
27
- (False, 1, False, False, np.array([[1, 5], [2, 1]], dtype=float)), # 90 deg rotation
28
- (False, 2, False, False, np.array([[5, 1], [1, 2]], dtype=float)), # 180 deg rotation
29
- (False, 0, True, False, np.array([[2, 1], [1, 5]], dtype=float)), # transposed
30
- (False, 0, False, True, np.array([[0.30103, 0.0], [0.0, 0.69897]], dtype=float)), # log
31
- (True, 0, False, False, np.array([[5.0, 3.0], [3.0, 9.0]], dtype=float)), # FFT
32
- ],
33
- )
34
- def test_on_image_update(
35
- qtbot,
36
- eiger_plot_instance,
37
- fft_checked,
38
- rotation_index,
39
- transpose_checked,
40
- log_checked,
41
- expected_image,
42
- ):
43
- # Initialize image and mask
44
- eiger_plot_instance.image = np.array([[1, 2], [3, 4]], dtype=float)
45
- eiger_plot_instance.mask = np.array([[0, 1], [1, 0]], dtype=float)
46
-
47
- # Mock UI elements
48
- eiger_plot_instance.checkBox_FFT = MagicMock()
49
- eiger_plot_instance.checkBox_FFT.isChecked.return_value = fft_checked
50
- eiger_plot_instance.comboBox_rotation = MagicMock()
51
- eiger_plot_instance.comboBox_rotation.currentIndex.return_value = rotation_index
52
- eiger_plot_instance.checkBox_transpose = MagicMock()
53
- eiger_plot_instance.checkBox_transpose.isChecked.return_value = transpose_checked
54
- eiger_plot_instance.checkBox_log = MagicMock()
55
- eiger_plot_instance.checkBox_log.isChecked.return_value = log_checked
56
- eiger_plot_instance.imageItem = MagicMock()
57
-
58
- # Call the method
59
- eiger_plot_instance.on_image_update()
60
-
61
- # Validate the transformations
62
- np.testing.assert_array_almost_equal(eiger_plot_instance.image, expected_image, decimal=5)
63
-
64
- # Validate that setImage was called
65
- eiger_plot_instance.imageItem.setImage.assert_called_with(
66
- eiger_plot_instance.image, autoLevels=False
67
- )
68
-
69
-
70
- def test_init_ui(eiger_plot_instance):
71
- assert isinstance(eiger_plot_instance.plot_item, pg.PlotItem)
72
- assert isinstance(eiger_plot_instance.imageItem, pg.ImageItem)
73
- assert isinstance(eiger_plot_instance.hist, pg.HistogramLUTItem)
74
-
75
-
76
- def test_start_zmq_consumer(eiger_plot_instance):
77
- with patch("threading.Thread") as MockThread:
78
- eiger_plot_instance.start_zmq_consumer()
79
- MockThread.assert_called_once()
80
- MockThread.return_value.start.assert_called_once()
81
-
82
-
83
- def test_zmq_consumer(eiger_plot_instance, qtbot):
84
- fake_meta = json.dumps({"type": "int32", "shape": (2, 2)}).encode("utf-8")
85
- fake_data = np.array([[1, 2], [3, 4]], dtype="int32").tobytes()
86
-
87
- with patch("zmq.Context", autospec=True) as MockContext:
88
- mock_socket = MagicMock()
89
- mock_socket.recv_multipart.side_effect = ((fake_meta, fake_data),)
90
- MockContext.return_value.socket.return_value = mock_socket
91
-
92
- # Mocking the update_signal to check if it gets emitted
93
- eiger_plot_instance.update_signal = MagicMock()
94
-
95
- with patch("zmq.Poller"):
96
- # will do only 1 iteration of the loop in the thread
97
- eiger_plot_instance._zmq_consumer_exit_event.set()
98
- # Run the method under test
99
- consumer_thread = eiger_plot_instance.start_zmq_consumer()
100
- consumer_thread.join()
101
-
102
- # Check if zmq methods are called
103
- # MockContext.assert_called_once()
104
- assert MockContext.call_count == 1
105
- mock_socket.connect.assert_called_with("tcp://129.129.95.38:20000")
106
- mock_socket.setsockopt_string.assert_called_with(zmq.SUBSCRIBE, "")
107
- mock_socket.recv_multipart.assert_called()
108
-
109
- # Check if update_signal was emitted
110
- eiger_plot_instance.update_signal.emit.assert_called_once()
111
-
112
- # Validate the image data
113
- np.testing.assert_array_equal(
114
- eiger_plot_instance.image, np.array([[1, 2], [3, 4]], dtype="int32")
115
- )