QuLab 2.11.7__py3-none-any.whl → 2.11.9__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.
- qulab/executor/analyze.py +1 -0
- qulab/executor/cli.py +224 -45
- qulab/executor/utils.py +16 -2
- qulab/monitor/__init__.py +1 -1
- qulab/monitor/__main__.py +31 -3
- qulab/monitor/config.py +55 -30
- qulab/monitor/dataset.py +145 -38
- qulab/monitor/event_queue.py +98 -25
- qulab/monitor/mainwindow.py +165 -131
- qulab/monitor/monitor.py +220 -30
- qulab/monitor/ploter.py +98 -73
- qulab/monitor/qt_compat.py +30 -1
- qulab/monitor/toolbar.py +152 -121
- qulab/utils.py +16 -17
- qulab/version.py +1 -1
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/METADATA +1 -1
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/RECORD +21 -21
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/WHEEL +1 -1
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/entry_points.txt +0 -0
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/licenses/LICENSE +0 -0
- {qulab-2.11.7.dist-info → qulab-2.11.9.dist-info}/top_level.txt +0 -0
qulab/monitor/mainwindow.py
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
QuLab Monitor Main Window Module
|
3
|
+
|
4
|
+
This module implements the main window interface for the QuLab monitor application.
|
5
|
+
It provides a scrollable grid layout of plots with configurable columns and
|
6
|
+
interactive features like axis linking and data transformation.
|
7
|
+
"""
|
8
|
+
|
1
9
|
from multiprocessing import Queue
|
2
10
|
from typing import Literal
|
3
11
|
|
4
|
-
from .config import
|
12
|
+
from .config import TRANSFORMS, ROLL_INDICES, STYLE
|
5
13
|
from .dataset import Dataset
|
6
14
|
from .event_queue import EventQueue
|
7
15
|
from .ploter import PlotWidget
|
@@ -12,223 +20,249 @@ from .toolbar import ToolBar
|
|
12
20
|
|
13
21
|
|
14
22
|
class MainWindow(QtWidgets.QMainWindow):
|
23
|
+
"""
|
24
|
+
Main window for the QuLab monitor application.
|
25
|
+
|
26
|
+
This window manages a grid of plot widgets and provides controls for data
|
27
|
+
visualization and interaction. It includes features like:
|
28
|
+
- Configurable number of columns in the plot grid
|
29
|
+
- Scrollable plot area
|
30
|
+
- Toolbar with plot controls
|
31
|
+
- Real-time data updates
|
32
|
+
- Axis linking between plots
|
33
|
+
- Data transformation options
|
34
|
+
|
35
|
+
Args:
|
36
|
+
queue: Multiprocessing queue for receiving data and commands
|
37
|
+
num_columns: Number of columns in the plot grid
|
38
|
+
plot_minimum_height: Minimum height for each plot widget
|
39
|
+
plot_colors: List of RGB color tuples for plot lines
|
40
|
+
"""
|
15
41
|
|
16
42
|
def __init__(self,
|
17
43
|
queue: Queue,
|
18
|
-
|
19
|
-
plot_minimum_height=350,
|
44
|
+
num_columns: int = 3,
|
45
|
+
plot_minimum_height: int = 350,
|
20
46
|
plot_colors: list[tuple[int, int, int]] | None = None):
|
21
47
|
super().__init__()
|
22
|
-
self.
|
23
|
-
self.
|
48
|
+
self.num_columns = num_columns
|
49
|
+
self.needs_reshuffle = False
|
24
50
|
self.plot_minimum_height = plot_minimum_height
|
25
51
|
self.plot_widgets: list[PlotWidget] = []
|
26
52
|
self.plot_colors = plot_colors
|
53
|
+
|
54
|
+
# Initialize components
|
27
55
|
self.toolbar = ToolBar()
|
28
56
|
self.trace_data_box = Dataset()
|
29
57
|
self.point_data_box = Dataset()
|
30
58
|
self.queue = EventQueue(queue, self.toolbar, self.point_data_box,
|
31
|
-
|
59
|
+
self.trace_data_box)
|
32
60
|
|
33
61
|
self.init_ui()
|
34
62
|
|
63
|
+
# Set up update timer
|
35
64
|
self.timer = QtCore.QTimer()
|
36
65
|
self.timer.timeout.connect(self.update)
|
37
|
-
self.timer.start(250)
|
66
|
+
self.timer.start(250) # Update every 250ms
|
38
67
|
|
39
68
|
def init_ui(self):
|
40
|
-
|
69
|
+
"""Initialize the user interface components."""
|
70
|
+
self.setStyleSheet(STYLE)
|
41
71
|
self.setMinimumHeight(500)
|
42
72
|
self.setMinimumWidth(700)
|
43
|
-
|
44
|
-
|
45
|
-
self.
|
46
|
-
|
73
|
+
|
74
|
+
# Create scroll area
|
75
|
+
self.scroll = QtWidgets.QScrollArea()
|
76
|
+
self.widget = QtWidgets.QWidget()
|
47
77
|
self.layout = QtWidgets.QGridLayout()
|
48
78
|
self.widget.setLayout(self.layout)
|
49
79
|
|
50
|
-
#
|
51
|
-
#self.setCorner(Qt.TopSection, Qt.TopDockWidgetArea);
|
80
|
+
# Configure scroll area
|
52
81
|
self.scroll.setVerticalScrollBarPolicy(ScrollBarAlwaysOn)
|
53
82
|
self.scroll.setHorizontalScrollBarPolicy(ScrollBarAlwaysOff)
|
54
83
|
self.scroll.setWidgetResizable(True)
|
55
84
|
self.scroll.setWidget(self.widget)
|
56
85
|
self.setCentralWidget(self.scroll)
|
57
86
|
|
87
|
+
# Set up toolbar dock widget
|
58
88
|
self.dock = QtWidgets.QDockWidget(self)
|
59
89
|
self.dock.setAllowedAreas(TopDockWidgetArea | BottomDockWidgetArea)
|
60
90
|
self.addDockWidget(TopDockWidgetArea, self.dock)
|
61
91
|
self.dock.setFloating(False)
|
62
92
|
self.dock.setWidget(self.toolbar)
|
63
93
|
self.dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable)
|
64
|
-
#self.tabifyDockWidget(self.dock,None);
|
65
|
-
#self.addDockWidget(self.dock);
|
66
|
-
#self.setStatusBar(self.toolbar);
|
67
|
-
#self.layout.addWidget(self.toolbar , 0 , 0 , 1, self.ncol);
|
68
94
|
|
69
|
-
self.setWindowTitle('
|
95
|
+
self.setWindowTitle('QuLab Monitor')
|
70
96
|
self.show()
|
71
97
|
self.toolbar.set_mainwindow(self)
|
72
98
|
self.toolbar.pb.setChecked(True)
|
73
99
|
|
74
100
|
@property
|
75
101
|
def mode(self) -> Literal["P", "T"]:
|
102
|
+
"""Current plotting mode (Points or Traces)."""
|
76
103
|
return self.toolbar.mode
|
77
104
|
|
78
105
|
@property
|
79
106
|
def dataset(self) -> Dataset:
|
107
|
+
"""Current active dataset based on mode."""
|
80
108
|
return {"P": self.point_data_box, "T": self.trace_data_box}[self.mode]
|
81
109
|
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
self.
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
self.plot_widgets
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
110
|
+
def set_num_columns(self, columns: int):
|
111
|
+
"""Set the number of columns in the plot grid."""
|
112
|
+
columns = max(1, min(10, int(columns)))
|
113
|
+
if columns != self.num_columns:
|
114
|
+
self.needs_reshuffle = True
|
115
|
+
self.num_columns = columns
|
116
|
+
|
117
|
+
def add_subplot(self) -> PlotWidget:
|
118
|
+
"""Add a new plot widget to the grid."""
|
119
|
+
plot_count = len(self.plot_widgets)
|
120
|
+
plot_widget = PlotWidget(self.plot_minimum_height, self.plot_colors)
|
121
|
+
self.plot_widgets.append(plot_widget)
|
122
|
+
|
123
|
+
grid_row = plot_count // self.num_columns
|
124
|
+
grid_col = plot_count % self.num_columns
|
125
|
+
self.layout.addWidget(plot_widget, grid_row + 1, grid_col)
|
126
|
+
return plot_widget
|
127
|
+
|
128
|
+
def create_subplots(self, xy_pairs: list[tuple[str, str]]):
|
129
|
+
"""Create multiple subplots with given X-Y axis pairs."""
|
130
|
+
for x_name, y_name in xy_pairs:
|
131
|
+
plot_widget = self.add_subplot()
|
132
|
+
plot_widget.set_x_label(x_name)
|
133
|
+
plot_widget.set_y_label(y_name)
|
102
134
|
self.do_link()
|
103
135
|
self.all_enable_auto_range()
|
104
136
|
|
105
137
|
def clear_subplots(self):
|
106
|
-
|
107
|
-
|
108
|
-
self.
|
138
|
+
"""Remove all plot widgets from the grid."""
|
139
|
+
for plot_widget in self.plot_widgets:
|
140
|
+
self.layout.removeWidget(plot_widget)
|
141
|
+
plot_widget.setParent(None)
|
109
142
|
self.plot_widgets.clear()
|
110
143
|
|
111
|
-
def remove_plot(self,
|
112
|
-
|
113
|
-
|
144
|
+
def remove_plot(self, widget: PlotWidget):
|
145
|
+
"""Remove a specific plot widget from the grid."""
|
146
|
+
widget.setParent(None)
|
147
|
+
self.plot_widgets.remove(widget)
|
114
148
|
self.reshuffle()
|
115
149
|
|
116
|
-
def drop_last_plot(self,
|
117
|
-
|
118
|
-
|
119
|
-
if
|
120
|
-
|
121
|
-
|
122
|
-
del
|
123
|
-
del self.plot_widgets[
|
150
|
+
def drop_last_plot(self, index: int = -1):
|
151
|
+
"""Remove the plot at the specified index."""
|
152
|
+
index = int(index)
|
153
|
+
if index < len(self.plot_widgets):
|
154
|
+
widget = self.plot_widgets[index]
|
155
|
+
widget.setParent(None)
|
156
|
+
del widget
|
157
|
+
del self.plot_widgets[index]
|
124
158
|
self.reshuffle()
|
125
159
|
|
126
160
|
def reshuffle(self):
|
161
|
+
"""Rearrange plot widgets in the grid."""
|
127
162
|
for idx, widget in enumerate(self.plot_widgets):
|
128
163
|
widget.setParent(None)
|
129
|
-
grid_row = idx // self.
|
130
|
-
grid_col = idx % self.
|
164
|
+
grid_row = idx // self.num_columns
|
165
|
+
grid_col = idx % self.num_columns
|
131
166
|
self.layout.addWidget(widget, grid_row + 1, grid_col)
|
132
167
|
|
133
|
-
def keyPressEvent(self,
|
134
|
-
|
135
|
-
|
136
|
-
if
|
137
|
-
self.
|
138
|
-
elif
|
139
|
-
self.
|
140
|
-
|
141
|
-
def mouse_click(self):
|
142
|
-
pass
|
168
|
+
def keyPressEvent(self, event):
|
169
|
+
"""Handle keyboard events for column adjustment."""
|
170
|
+
key = event.text()
|
171
|
+
if key in ['_', '-']:
|
172
|
+
self.set_num_columns(self.num_columns - 1)
|
173
|
+
elif key in ['=', '+']:
|
174
|
+
self.set_num_columns(self.num_columns + 1)
|
143
175
|
|
144
176
|
def do_link(self):
|
145
|
-
"""
|
146
|
-
|
147
|
-
|
148
|
-
share the same x or y axis
|
149
|
-
"""
|
150
|
-
same_X = {}
|
177
|
+
"""Link plots that share the same X or Y axis."""
|
178
|
+
same_x_axis = {}
|
151
179
|
xy_pairs = self.toolbar.xypairs
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
180
|
+
|
181
|
+
# Group plots by X axis
|
182
|
+
for idx, (x_name, y_name) in enumerate(xy_pairs):
|
183
|
+
if x_name not in same_x_axis:
|
184
|
+
same_x_axis[x_name] = []
|
185
|
+
same_x_axis[x_name].append(idx)
|
186
|
+
|
187
|
+
share_x, share_y = self.toolbar.sharexy()
|
188
|
+
should_unlink = not (share_x and share_y)
|
189
|
+
|
190
|
+
# Link or unlink axes
|
191
|
+
for x_name, plot_indices in same_x_axis.items():
|
192
|
+
prev_idx = -1
|
193
|
+
for curr_idx in plot_indices:
|
194
|
+
if prev_idx != -1:
|
195
|
+
if should_unlink:
|
196
|
+
self.plot_widgets[prev_idx].plotItem.vb.setXLink(None)
|
197
|
+
self.plot_widgets[prev_idx].plotItem.vb.setYLink(None)
|
198
|
+
|
199
|
+
if share_x:
|
200
|
+
self.plot_widgets[prev_idx].plotItem.vb.setXLink(
|
201
|
+
self.plot_widgets[curr_idx].plotItem.vb)
|
202
|
+
|
203
|
+
if share_y:
|
204
|
+
self.plot_widgets[prev_idx].plotItem.vb.setYLink(
|
205
|
+
self.plot_widgets[curr_idx].plotItem.vb)
|
206
|
+
prev_idx = curr_idx
|
177
207
|
|
178
208
|
def all_auto_range(self):
|
179
|
-
|
180
|
-
|
209
|
+
"""Auto-range all plot widgets."""
|
210
|
+
for plot_widget in self.plot_widgets:
|
211
|
+
plot_widget.auto_range()
|
181
212
|
|
182
213
|
def all_enable_auto_range(self):
|
183
|
-
for
|
184
|
-
|
214
|
+
"""Enable auto-range for all plot widgets."""
|
215
|
+
for plot_widget in self.plot_widgets:
|
216
|
+
plot_widget.enable_auto_range()
|
185
217
|
|
186
218
|
def update(self):
|
187
|
-
|
219
|
+
"""Update plots with new data and handle UI changes."""
|
188
220
|
self.queue.flush()
|
221
|
+
needs_rescale = False
|
189
222
|
|
190
|
-
|
191
|
-
|
192
|
-
# setup the xyfm
|
193
|
-
if (self.toolbar.xypairs_dirty):
|
223
|
+
# Handle plot layout changes
|
224
|
+
if self.toolbar.xypairs_dirty:
|
194
225
|
self.clear_subplots()
|
195
226
|
self.create_subplots(self.toolbar.xypairs)
|
196
227
|
self.toolbar.xypairs_dirty = False
|
197
|
-
|
228
|
+
needs_rescale = True
|
198
229
|
|
199
|
-
if
|
230
|
+
if self.toolbar.link_dirty:
|
200
231
|
self.do_link()
|
201
232
|
self.toolbar.link_dirty = False
|
202
233
|
|
203
|
-
if
|
204
|
-
self.
|
234
|
+
if self.needs_reshuffle:
|
235
|
+
self.needs_reshuffle = False
|
205
236
|
self.reshuffle()
|
206
237
|
|
207
|
-
#
|
208
|
-
if
|
209
|
-
for
|
210
|
-
|
211
|
-
|
238
|
+
# Update plot settings
|
239
|
+
if self.toolbar.xyfm_dirty:
|
240
|
+
for plot_widget in self.plot_widgets:
|
241
|
+
plot_widget.plotItem.ctrl.logXCheck.setChecked(self.toolbar.lx)
|
242
|
+
plot_widget.plotItem.ctrl.logYCheck.setChecked(self.toolbar.ly)
|
212
243
|
|
213
|
-
#
|
214
|
-
|
215
|
-
if (self.toolbar.CR_flag):
|
244
|
+
# Handle data updates
|
245
|
+
if self.toolbar.CR_flag:
|
216
246
|
self.toolbar.CR_flag = False
|
217
247
|
self.dataset.clear_history()
|
218
248
|
self.dataset.dirty = True
|
219
249
|
|
220
|
-
if
|
250
|
+
if self.dataset.dirty or self.toolbar.xyfm_dirty or needs_rescale:
|
221
251
|
self.dataset.dirty = False
|
222
|
-
self.xyfm_dirty = False
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
252
|
+
self.toolbar.xyfm_dirty = False
|
253
|
+
|
254
|
+
# Update plot data
|
255
|
+
for plot_widget in self.plot_widgets:
|
256
|
+
x_transform = TRANSFORMS[self.toolbar.fx]
|
257
|
+
y_transform = TRANSFORMS[self.toolbar.fy]
|
258
|
+
|
259
|
+
for idx in ROLL_INDICES:
|
260
|
+
x_data, y_data = self.dataset.get_data(idx, plot_widget.x_name, plot_widget.y_name)
|
261
|
+
data_length = min(len(x_data), len(y_data))
|
262
|
+
x_data = x_transform(x_data[:data_length], 0)
|
263
|
+
y_data = y_transform(y_data[:data_length], 0)
|
264
|
+
plot_widget.set_data(idx, x_data, y_data)
|
265
|
+
|
266
|
+
plot_widget.update()
|
267
|
+
if needs_rescale:
|
268
|
+
plot_widget.auto_range()
|