bec-widgets 2.16.2__py3-none-any.whl → 2.17.0__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.
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='ScanProgressBar' name='scan_progress_bar'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class ScanProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = ScanProgressBar(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "BEC Utils"
31
+
32
+ def icon(self):
33
+ return designer_material_icon(ScanProgressBar.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "scan_progress_bar"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "ScanProgressBar"
49
+
50
+ def toolTip(self):
51
+ return "A progress bar that is hooked up to the scan progress of a scan."
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import os
5
+ import time
6
+ from typing import Literal
7
+
8
+ import numpy as np
9
+ from bec_lib.endpoints import MessageEndpoints
10
+ from bec_lib.logger import bec_logger
11
+ from qtpy.QtCore import QObject, QTimer, Signal
12
+ from qtpy.QtWidgets import QVBoxLayout, QWidget
13
+
14
+ from bec_widgets.utils.bec_widget import BECWidget
15
+ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
+ from bec_widgets.utils.ui_loader import UILoader
17
+ from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import ProgressState
18
+
19
+ logger = bec_logger.logger
20
+
21
+
22
+ class ProgressSource(enum.Enum):
23
+ """
24
+ Enum to define the source of the progress.
25
+ """
26
+
27
+ SCAN_PROGRESS = "scan_progress"
28
+ DEVICE_PROGRESS = "device_progress"
29
+
30
+
31
+ class ProgressTask(QObject):
32
+ """
33
+ Class to store progress information.
34
+ Inspired by https://github.com/Textualize/rich/blob/master/rich/progress.py
35
+ """
36
+
37
+ def __init__(self, parent: QWidget, value: float = 0, max_value: float = 0, done: bool = False):
38
+ super().__init__(parent=parent)
39
+ self.start_time = time.time()
40
+ self.done = done
41
+ self.value = value
42
+ self.max_value = max_value
43
+ self._elapsed_time = 0
44
+
45
+ self.timer = QTimer(self)
46
+ self.timer.timeout.connect(self.update_elapsed_time)
47
+ self.timer.start(100) # update the elapsed time every 100 ms
48
+
49
+ def update(self, value: float, max_value: float, done: bool = False):
50
+ """
51
+ Update the progress.
52
+ """
53
+ self.max_value = max_value
54
+ self.done = done
55
+ self.value = value
56
+ if done:
57
+ self.timer.stop()
58
+
59
+ def update_elapsed_time(self):
60
+ """
61
+ Update the time estimates. This is called every 100 ms by a QTimer.
62
+ """
63
+ self._elapsed_time += 0.1
64
+
65
+ @property
66
+ def percentage(self) -> float:
67
+ """float: Get progress of task as a percentage. If a None total was set, returns 0"""
68
+ if not self.max_value:
69
+ return 0.0
70
+ completed = (self.value / self.max_value) * 100.0
71
+ completed = min(100.0, max(0.0, completed))
72
+ return completed
73
+
74
+ @property
75
+ def speed(self) -> float:
76
+ """Get the estimated speed in steps per second."""
77
+ if self._elapsed_time == 0:
78
+ return 0.0
79
+
80
+ return self.value / self._elapsed_time
81
+
82
+ @property
83
+ def frequency(self) -> float:
84
+ """Get the estimated frequency in steps per second."""
85
+ if self.speed == 0:
86
+ return 0.0
87
+ return 1 / self.speed
88
+
89
+ @property
90
+ def time_elapsed(self) -> str:
91
+ # format the elapsed time to a string in the format HH:MM:SS
92
+ return self._format_time(int(self._elapsed_time))
93
+
94
+ @property
95
+ def remaining(self) -> float:
96
+ """Get the estimated remaining steps."""
97
+ if self.done:
98
+ return 0.0
99
+ remaining = self.max_value - self.value
100
+ return remaining
101
+
102
+ @property
103
+ def time_remaining(self) -> str:
104
+ """
105
+ Get the estimated remaining time in the format HH:MM:SS.
106
+ """
107
+ if self.done or not self.speed or not self.remaining:
108
+ return self._format_time(0)
109
+ estimate = int(np.round(self.remaining / self.speed))
110
+
111
+ return self._format_time(estimate)
112
+
113
+ def _format_time(self, seconds: float) -> str:
114
+ """
115
+ Format the time in seconds to a string in the format HH:MM:SS.
116
+ """
117
+ return f"{seconds // 3600:02}:{(seconds // 60) % 60:02}:{seconds % 60:02}"
118
+
119
+
120
+ class ScanProgressBar(BECWidget, QWidget):
121
+ """
122
+ Widget to display a progress bar that is hooked up to the scan progress of a scan.
123
+ If you want to manually set the progress, it is recommended to use the BECProgressbar or QProgressbar directly.
124
+ """
125
+
126
+ ICON_NAME = "timelapse"
127
+ progress_started = Signal()
128
+ progress_finished = Signal()
129
+
130
+ def __init__(self, parent=None, client=None, config=None, gui_id=None, one_line_design=False):
131
+ super().__init__(parent=parent, client=client, config=config, gui_id=gui_id)
132
+
133
+ self.get_bec_shortcuts()
134
+ ui_file = os.path.join(
135
+ os.path.dirname(__file__),
136
+ "scan_progressbar_one_line.ui" if one_line_design else "scan_progressbar.ui",
137
+ )
138
+ self.ui = UILoader(self).loader(ui_file)
139
+ self.layout = QVBoxLayout(self)
140
+ self.layout.setSpacing(0)
141
+ self.layout.setContentsMargins(0, 0, 0, 0)
142
+ self.layout.addWidget(self.ui)
143
+ self.setLayout(self.layout)
144
+ self.progressbar = self.ui.progressbar
145
+
146
+ self.connect_to_queue()
147
+ self._progress_source = None
148
+ self.task = None
149
+ self.scan_number = None
150
+ self.progress_started.connect(lambda: print("Scan progress started"))
151
+
152
+ def connect_to_queue(self):
153
+ """
154
+ Connect to the queue status signal.
155
+ """
156
+ self.bec_dispatcher.connect_slot(self.on_queue_update, MessageEndpoints.scan_queue_status())
157
+
158
+ def set_progress_source(self, source: ProgressSource, device=None):
159
+ """
160
+ Set the source of the progress.
161
+ """
162
+ if self._progress_source == source:
163
+ self.update_source_label(source, device=device)
164
+ return
165
+ if self._progress_source is not None:
166
+ self.bec_dispatcher.disconnect_slot(
167
+ self.on_progress_update,
168
+ (
169
+ MessageEndpoints.scan_progress()
170
+ if self._progress_source == ProgressSource.SCAN_PROGRESS
171
+ else MessageEndpoints.device_progress(device=device)
172
+ ),
173
+ )
174
+ self._progress_source = source
175
+ self.bec_dispatcher.connect_slot(
176
+ self.on_progress_update,
177
+ (
178
+ MessageEndpoints.scan_progress()
179
+ if source == ProgressSource.SCAN_PROGRESS
180
+ else MessageEndpoints.device_progress(device=device)
181
+ ),
182
+ )
183
+ self.update_source_label(source, device=device)
184
+ # self.progress_started.emit()
185
+
186
+ def update_source_label(self, source: ProgressSource, device=None):
187
+ scan_text = f"Scan {self.scan_number}" if self.scan_number is not None else "Scan"
188
+ text = scan_text if source == ProgressSource.SCAN_PROGRESS else f"Device {device}"
189
+ logger.info(f"Set progress source to {text}")
190
+ self.ui.source_label.setText(text)
191
+
192
+ @SafeSlot(dict, dict)
193
+ def on_progress_update(self, msg_content: dict, metadata: dict):
194
+ """
195
+ Update the progress bar based on the progress message.
196
+ """
197
+ value = msg_content["value"]
198
+ max_value = msg_content.get("max_value", 100)
199
+ done = msg_content.get("done", False)
200
+ status: Literal["open", "paused", "aborted", "halted", "closed"] = metadata.get(
201
+ "status", "open"
202
+ )
203
+
204
+ if self.task is None:
205
+ return
206
+ self.task.update(value, max_value, done)
207
+
208
+ self.update_labels()
209
+
210
+ self.progressbar.set_maximum(self.task.max_value)
211
+ self.progressbar.state = ProgressState.from_bec_status(status)
212
+ self.progressbar.set_value(self.task.value)
213
+
214
+ if done:
215
+ self.task = None
216
+ self.progress_finished.emit()
217
+ return
218
+
219
+ @SafeProperty(bool)
220
+ def show_elapsed_time(self):
221
+ return self.ui.elapsed_time_label.isVisible()
222
+
223
+ @show_elapsed_time.setter
224
+ def show_elapsed_time(self, value):
225
+ self.ui.elapsed_time_label.setVisible(value)
226
+ if hasattr(self.ui, "dash"):
227
+ self.ui.dash.setVisible(value)
228
+
229
+ @SafeProperty(bool)
230
+ def show_remaining_time(self):
231
+ return self.ui.remaining_time_label.isVisible()
232
+
233
+ @show_remaining_time.setter
234
+ def show_remaining_time(self, value):
235
+ self.ui.remaining_time_label.setVisible(value)
236
+ if hasattr(self.ui, "dash"):
237
+ self.ui.dash.setVisible(value)
238
+
239
+ @SafeProperty(bool)
240
+ def show_source_label(self):
241
+ return self.ui.source_label.isVisible()
242
+
243
+ @show_source_label.setter
244
+ def show_source_label(self, value):
245
+ self.ui.source_label.setVisible(value)
246
+
247
+ def update_labels(self):
248
+ """
249
+ Update the labels based on the progress task.
250
+ """
251
+ if self.task is None:
252
+ return
253
+
254
+ self.ui.elapsed_time_label.setText(self.task.time_elapsed)
255
+ self.ui.remaining_time_label.setText(self.task.time_remaining)
256
+
257
+ @SafeSlot(dict, dict, verify_sender=True)
258
+ def on_queue_update(self, msg_content, metadata):
259
+ """
260
+ Update the progress bar based on the queue status.
261
+ """
262
+ if not "queue" in msg_content:
263
+ return
264
+ primary_queue_info = msg_content["queue"].get("primary", {}).get("info", [])
265
+ if len(primary_queue_info) == 0:
266
+ return
267
+ scan_info = primary_queue_info[0]
268
+ if scan_info is None:
269
+ return
270
+ if scan_info.get("status").lower() == "running" and self.task is None:
271
+ self.task = ProgressTask(parent=self)
272
+ self.progress_started.emit()
273
+
274
+ active_request_block = scan_info.get("active_request_block", {})
275
+ if active_request_block is None:
276
+ return
277
+
278
+ self.scan_number = active_request_block.get("scan_number")
279
+ report_instructions = active_request_block.get("report_instructions", [])
280
+ if not report_instructions:
281
+ return
282
+
283
+ # for now, let's just use the first instruction
284
+ instruction = report_instructions[0]
285
+
286
+ if "scan_progress" in instruction:
287
+ self.set_progress_source(ProgressSource.SCAN_PROGRESS)
288
+ elif "device_progress" in instruction:
289
+ device = instruction["device_progress"][0]
290
+ self.set_progress_source(ProgressSource.DEVICE_PROGRESS, device=device)
291
+
292
+ def cleanup(self):
293
+ if self.task is not None:
294
+ self.task.timer.stop()
295
+ self.close()
296
+ self.deleteLater()
297
+ if self._progress_source is not None:
298
+ self.bec_dispatcher.disconnect_slot(
299
+ self.on_progress_update,
300
+ (
301
+ MessageEndpoints.scan_progress()
302
+ if self._progress_source == ProgressSource.SCAN_PROGRESS
303
+ else MessageEndpoints.device_progress(device=self._progress_source.value)
304
+ ),
305
+ )
306
+ self.progressbar.close()
307
+ self.progressbar.deleteLater()
308
+ super().cleanup()
309
+
310
+
311
+ if __name__ == "__main__": # pragma: no cover
312
+ from qtpy.QtWidgets import QApplication
313
+
314
+ bec_logger.disabled_modules = ["bec_lib"]
315
+ app = QApplication([])
316
+
317
+ widget = ScanProgressBar()
318
+ widget.show()
319
+
320
+ app.exec_()
@@ -0,0 +1,141 @@
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>211</width>
10
+ <height>60</height>
11
+ </rect>
12
+ </property>
13
+ <property name="sizePolicy">
14
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
15
+ <horstretch>0</horstretch>
16
+ <verstretch>0</verstretch>
17
+ </sizepolicy>
18
+ </property>
19
+ <property name="minimumSize">
20
+ <size>
21
+ <width>0</width>
22
+ <height>60</height>
23
+ </size>
24
+ </property>
25
+ <property name="maximumSize">
26
+ <size>
27
+ <width>16777215</width>
28
+ <height>16777215</height>
29
+ </size>
30
+ </property>
31
+ <property name="windowTitle">
32
+ <string>Form</string>
33
+ </property>
34
+ <layout class="QVBoxLayout" name="verticalLayout">
35
+ <property name="spacing">
36
+ <number>1</number>
37
+ </property>
38
+ <property name="leftMargin">
39
+ <number>2</number>
40
+ </property>
41
+ <property name="topMargin">
42
+ <number>0</number>
43
+ </property>
44
+ <property name="rightMargin">
45
+ <number>2</number>
46
+ </property>
47
+ <property name="bottomMargin">
48
+ <number>0</number>
49
+ </property>
50
+ <item>
51
+ <layout class="QHBoxLayout" name="source_layout">
52
+ <item>
53
+ <widget class="QLabel" name="source_label">
54
+ <property name="maximumSize">
55
+ <size>
56
+ <width>16777215</width>
57
+ <height>20</height>
58
+ </size>
59
+ </property>
60
+ <property name="text">
61
+ <string>Scan</string>
62
+ </property>
63
+ </widget>
64
+ </item>
65
+ <item>
66
+ <spacer name="source_spacer">
67
+ <property name="orientation">
68
+ <enum>Qt::Orientation::Horizontal</enum>
69
+ </property>
70
+ <property name="sizeHint" stdset="0">
71
+ <size>
72
+ <width>40</width>
73
+ <height>10</height>
74
+ </size>
75
+ </property>
76
+ </spacer>
77
+ </item>
78
+ </layout>
79
+ </item>
80
+ <item>
81
+ <widget class="BECProgressBar" name="progressbar">
82
+ <property name="padding_left_right" stdset="0">
83
+ <double>2.000000000000000</double>
84
+ </property>
85
+ </widget>
86
+ </item>
87
+ <item>
88
+ <layout class="QHBoxLayout" name="timer_layout">
89
+ <item>
90
+ <widget class="QLabel" name="remaining_time_label">
91
+ <property name="maximumSize">
92
+ <size>
93
+ <width>16777215</width>
94
+ <height>20</height>
95
+ </size>
96
+ </property>
97
+ <property name="text">
98
+ <string>0:00:00</string>
99
+ </property>
100
+ </widget>
101
+ </item>
102
+ <item>
103
+ <spacer name="timer_spacer">
104
+ <property name="orientation">
105
+ <enum>Qt::Orientation::Horizontal</enum>
106
+ </property>
107
+ <property name="sizeHint" stdset="0">
108
+ <size>
109
+ <width>40</width>
110
+ <height>0</height>
111
+ </size>
112
+ </property>
113
+ </spacer>
114
+ </item>
115
+ <item>
116
+ <widget class="QLabel" name="elapsed_time_label">
117
+ <property name="maximumSize">
118
+ <size>
119
+ <width>16777215</width>
120
+ <height>20</height>
121
+ </size>
122
+ </property>
123
+ <property name="text">
124
+ <string>0:00:00</string>
125
+ </property>
126
+ </widget>
127
+ </item>
128
+ </layout>
129
+ </item>
130
+ </layout>
131
+ </widget>
132
+ <customwidgets>
133
+ <customwidget>
134
+ <class>BECProgressBar</class>
135
+ <extends>QWidget</extends>
136
+ <header>bec_progress_bar</header>
137
+ </customwidget>
138
+ </customwidgets>
139
+ <resources/>
140
+ <connections/>
141
+ </ui>
@@ -0,0 +1,124 @@
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>328</width>
10
+ <height>24</height>
11
+ </rect>
12
+ </property>
13
+ <property name="sizePolicy">
14
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
15
+ <horstretch>0</horstretch>
16
+ <verstretch>0</verstretch>
17
+ </sizepolicy>
18
+ </property>
19
+ <property name="minimumSize">
20
+ <size>
21
+ <width>0</width>
22
+ <height>24</height>
23
+ </size>
24
+ </property>
25
+ <property name="maximumSize">
26
+ <size>
27
+ <width>16777215</width>
28
+ <height>24</height>
29
+ </size>
30
+ </property>
31
+ <property name="windowTitle">
32
+ <string>Form</string>
33
+ </property>
34
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0">
35
+ <property name="leftMargin">
36
+ <number>0</number>
37
+ </property>
38
+ <property name="topMargin">
39
+ <number>2</number>
40
+ </property>
41
+ <property name="rightMargin">
42
+ <number>2</number>
43
+ </property>
44
+ <property name="bottomMargin">
45
+ <number>2</number>
46
+ </property>
47
+ <item>
48
+ <widget class="QLabel" name="source_label">
49
+ <property name="maximumSize">
50
+ <size>
51
+ <width>16777215</width>
52
+ <height>20</height>
53
+ </size>
54
+ </property>
55
+ <property name="text">
56
+ <string>Scan</string>
57
+ </property>
58
+ </widget>
59
+ </item>
60
+ <item>
61
+ <widget class="BECProgressBar" name="progressbar">
62
+ <property name="minimumSize">
63
+ <size>
64
+ <width>30</width>
65
+ <height>0</height>
66
+ </size>
67
+ </property>
68
+ <property name="padding_left_right" stdset="0">
69
+ <double>5.000000000000000</double>
70
+ </property>
71
+ </widget>
72
+ </item>
73
+ <item>
74
+ <layout class="QHBoxLayout" name="horizontalLayout">
75
+ <item>
76
+ <widget class="QLabel" name="remaining_time_label">
77
+ <property name="maximumSize">
78
+ <size>
79
+ <width>16777215</width>
80
+ <height>20</height>
81
+ </size>
82
+ </property>
83
+ <property name="text">
84
+ <string>0:00:00</string>
85
+ </property>
86
+ </widget>
87
+ </item>
88
+ <item>
89
+ <widget class="QLabel" name="dash">
90
+ <property name="text">
91
+ <string>-</string>
92
+ </property>
93
+ <property name="alignment">
94
+ <set>Qt::AlignmentFlag::AlignCenter</set>
95
+ </property>
96
+ </widget>
97
+ </item>
98
+ <item>
99
+ <widget class="QLabel" name="elapsed_time_label">
100
+ <property name="maximumSize">
101
+ <size>
102
+ <width>16777215</width>
103
+ <height>20</height>
104
+ </size>
105
+ </property>
106
+ <property name="text">
107
+ <string>0:00:00</string>
108
+ </property>
109
+ </widget>
110
+ </item>
111
+ </layout>
112
+ </item>
113
+ </layout>
114
+ </widget>
115
+ <customwidgets>
116
+ <customwidget>
117
+ <class>BECProgressBar</class>
118
+ <extends>QWidget</extends>
119
+ <header>bec_progress_bar</header>
120
+ </customwidget>
121
+ </customwidgets>
122
+ <resources/>
123
+ <connections/>
124
+ </ui>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.16.2
3
+ Version: 2.17.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -9,8 +9,8 @@ Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Topic :: Scientific/Engineering
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: bec-ipython-client<=4.0,>=3.38
13
- Requires-Dist: bec-lib<=4.0,>=3.38
12
+ Requires-Dist: bec-ipython-client<=4.0,>=3.42.4
13
+ Requires-Dist: bec-lib<=4.0,>=3.42.4
14
14
  Requires-Dist: bec-qthemes>=0.7,~=0.7
15
15
  Requires-Dist: black~=25.0
16
16
  Requires-Dist: isort>=5.13.2,~=5.13