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/monitor/monitor.py CHANGED
@@ -1,5 +1,17 @@
1
+ """
2
+ QuLab Monitor Module
3
+
4
+ This module provides real-time data visualization capabilities for QuLab.
5
+ It implements a multiprocessing-based monitoring system that can display
6
+ multiple data streams in a configurable grid layout.
7
+
8
+ Classes:
9
+ Monitor: Main class for creating and managing the monitoring window
10
+ """
11
+
1
12
  import multiprocessing as mp
2
13
  import sys
14
+ import zmq
3
15
 
4
16
  # try:
5
17
  # mp.set_start_method("spawn")
@@ -7,19 +19,40 @@ import sys
7
19
  # pass
8
20
 
9
21
 
10
- def main(queue: mp.Queue,
11
- ncols: int = 4,
22
+ def main(data_queue: mp.Queue,
23
+ num_columns: int = 4,
12
24
  minimum_height: int = 400,
13
- colors: list[tuple[int, int, int]] = []):
25
+ plot_colors: list[tuple[int, int, int]] = []) -> None:
26
+ """
27
+ Initialize and run the main monitoring window.
28
+
29
+ Args:
30
+ data_queue: Multiprocessing queue for data communication
31
+ num_columns: Number of columns in the plot grid layout
32
+ minimum_height: Minimum height of each plot in pixels
33
+ plot_colors: List of RGB color tuples for plot lines
34
+ """
14
35
  from .mainwindow import MainWindow
15
36
  from .qt_compat import QtWidgets
16
37
 
17
38
  app = QtWidgets.QApplication(sys.argv)
18
- main = MainWindow(queue, ncols, minimum_height, colors)
39
+ main_window = MainWindow(data_queue, num_columns, minimum_height, plot_colors)
19
40
  sys.exit(app.exec())
20
41
 
21
42
 
22
- class Monitor():
43
+ class MonitorUI:
44
+ """
45
+ Real-time data monitoring interface.
46
+
47
+ This class manages a separate process that displays real-time data plots
48
+ in a grid layout. Data can be added through various methods and will be
49
+ displayed immediately.
50
+
51
+ Args:
52
+ number_of_columns: Number of columns in the plot grid layout
53
+ minimum_height: Minimum height of each plot in pixels
54
+ colors: List of RGB color tuples for plot lines
55
+ """
23
56
 
24
57
  def __init__(self,
25
58
  number_of_columns: int = 4,
@@ -32,72 +65,203 @@ class Monitor():
32
65
  self.process = None
33
66
  self.start()
34
67
 
35
- def start(self):
68
+ def start(self) -> None:
69
+ """Start the monitoring process if not already running."""
36
70
  if self.process is not None and self.process.is_alive():
37
71
  return
38
72
  self.queue = mp.Queue(20)
39
73
  self.process = mp.Process(target=main,
40
- args=(self.queue, self.number_of_columns,
41
- self.minimum_height, self.colors))
74
+ args=(self.queue, self.number_of_columns,
75
+ self.minimum_height, self.colors))
42
76
  self.process.start()
43
77
 
44
- def _put(self, w: tuple):
45
- self.queue.put(w)
78
+ def _put(self, message: tuple) -> None:
79
+ """Send a message to the monitoring process."""
80
+ self.queue.put(message)
46
81
 
47
- def roll(self):
82
+ def roll(self) -> None:
83
+ """Clear and reset all plots."""
48
84
  self._put(("ROLL", None))
49
85
 
50
- def set_column_names(self, *arg):
51
- self._put(('PN', list(arg)))
86
+ def set_column_names(self, *column_names) -> None:
87
+ """Set the names of data columns for plotting."""
88
+ self._put(('PN', list(column_names)))
52
89
 
53
- def add_point(self, *arg):
54
- self._put(('PD', list(arg)))
90
+ def add_point(self, *values) -> None:
91
+ """Add a new data point to the plots."""
92
+ self._put(('PD', list(values)))
55
93
 
56
- def set_plots(self, arg):
94
+ def set_plots(self, plot_config: str) -> None:
57
95
  """
58
- arg: str, like "(x,y)" or "(x1,y1);(x2,y2);"
96
+ Configure which columns to plot against each other.
97
+
98
+ Args:
99
+ plot_config: String specifying plot configurations, e.g. "(x,y)" or "(x1,y1);(x2,y2);"
59
100
  """
60
- self._put(('PXY', str(arg)))
101
+ self._put(('PXY', str(plot_config)))
61
102
 
62
- def set_trace_column_names(self, *arg):
63
- self._put(('TN', list(arg)))
103
+ def set_trace_column_names(self, *column_names) -> None:
104
+ """Set the names of trace data columns."""
105
+ self._put(('TN', list(column_names)))
64
106
 
65
- def add_trace(self, *arg):
66
- self._put(('TD', list(arg)))
107
+ def add_trace(self, *values) -> None:
108
+ """Add a new trace data point."""
109
+ self._put(('TD', list(values)))
67
110
 
68
- def set_trace_plots(self, arg):
111
+ def set_trace_plots(self, plot_config: str) -> None:
69
112
  """
70
- arg: str, like "(x,y)" or "(x1,y1);(x2,y2);"
113
+ Configure which columns to plot for traces.
114
+
115
+ Args:
116
+ plot_config: String specifying trace plot configurations, e.g. "(x,y)" or "(x1,y1);(x2,y2);"
71
117
  """
72
- self._put(('TXY', str(arg)))
118
+ self._put(('TXY', str(plot_config)))
119
+
120
+ def is_alive(self) -> bool:
121
+ """Check if the monitoring process is running."""
122
+ return self.process.is_alive()
73
123
 
74
124
  def __del__(self):
125
+ """Clean up resources when the Monitor object is deleted."""
75
126
  try:
76
127
  self.process.kill()
77
128
  except:
78
129
  pass
79
130
 
80
- def is_alive(self):
81
- return self.process.is_alive()
131
+
132
+ class Monitor:
133
+ def __init__(self, address: str="127.0.0.1", port: int=5555):
134
+ self.context = zmq.Context()
135
+ self.socket = self.context.socket(zmq.REQ)
136
+ self.socket.connect(f"tcp://{address}:{port}")
137
+
138
+ def _send_command(self, cmd: str, data=None) -> None:
139
+ try:
140
+ self.socket.send_pyobj((cmd, data))
141
+ response = self.socket.recv_string()
142
+ if response.startswith("Error"):
143
+ raise RuntimeError(response)
144
+ except Exception as e:
145
+ raise RuntimeError(f"Failed to send command: {str(e)}")
146
+
147
+ def roll(self) -> None:
148
+ self._send_command("ROLL", None)
149
+
150
+ def set_column_names(self, *column_names) -> None:
151
+ self._send_command("PN", list(column_names))
152
+
153
+ def add_point(self, *values) -> None:
154
+ self._send_command("PD", list(values))
155
+
156
+ def set_plots(self, plot_config: str) -> None:
157
+ self._send_command("PXY", plot_config)
158
+
159
+ def set_trace_column_names(self, *column_names) -> None:
160
+ self._send_command("TN", list(column_names))
161
+
162
+ def add_trace(self, *values) -> None:
163
+ self._send_command("TD", list(values))
164
+
165
+ def set_trace_plots(self, plot_config: str) -> None:
166
+ self._send_command("TXY", plot_config)
167
+
168
+ def __del__(self):
169
+ try:
170
+ self.socket.close()
171
+ self.context.term()
172
+ except:
173
+ pass
82
174
 
83
175
 
176
+ class MonitorServer:
177
+ def __init__(self, address: str="*", port: int=5555, number_of_columns: int = 4,
178
+ minimum_height: int = 400,
179
+ colors: list[tuple[int, int, int]] = []):
180
+ self.address = address
181
+ self.port = port
182
+ self.number_of_columns = number_of_columns
183
+ self.minimum_height = minimum_height
184
+ self.colors = colors
185
+ self.running = True
186
+ self.process = mp.Process(target=self._run)
187
+ self.process.start()
188
+
189
+ def _run(self):
190
+ try:
191
+ # Create Monitor instance in the child process
192
+ self.monitor = MonitorUI(self.number_of_columns, self.minimum_height, self.colors)
193
+
194
+ # Create ZMQ context and socket in the child process
195
+ self.context = zmq.Context()
196
+ self.socket = self.context.socket(zmq.REP)
197
+ self.socket.bind(f"tcp://{self.address}:{self.port}")
198
+
199
+ while self.running:
200
+ try:
201
+ message = self.socket.recv_pyobj()
202
+ cmd, data = message
203
+ if cmd == 'ROLL':
204
+ self.monitor.roll()
205
+ elif cmd == 'PN':
206
+ self.monitor.set_column_names(*data)
207
+ elif cmd == 'PD':
208
+ self.monitor.add_point(*data)
209
+ elif cmd == 'PXY':
210
+ self.monitor.set_plots(data)
211
+ elif cmd == 'TN':
212
+ self.monitor.set_trace_column_names(*data)
213
+ elif cmd == 'TD':
214
+ self.monitor.add_trace(*data)
215
+ elif cmd == 'TXY':
216
+ self.monitor.set_trace_plots(data)
217
+ self.socket.send_string("OK")
218
+ except Exception as e:
219
+ self.socket.send_string(f"Error: {str(e)}")
220
+ finally:
221
+ # Clean up resources in child process
222
+ try:
223
+ self.socket.close()
224
+ self.context.term()
225
+ except:
226
+ pass
227
+
228
+ def __del__(self):
229
+ self.running = False
230
+ try:
231
+ self.process.terminate()
232
+ self.process.join()
233
+ except:
234
+ pass
235
+
236
+
237
+ # Global monitor instance
84
238
  _monitor = None
85
239
 
86
240
 
87
- def get_monitor(auto_open=True):
241
+ def get_monitor(auto_open: bool = True) -> MonitorUI:
242
+ """
243
+ Get or create a global Monitor instance.
244
+
245
+ Args:
246
+ auto_open: If True, create a new Monitor if none exists or if existing one is not running
247
+
248
+ Returns:
249
+ Monitor instance
250
+ """
88
251
  global _monitor
89
252
 
90
253
  if auto_open and (_monitor is None or not _monitor.is_alive()):
91
- _monitor = Monitor()
254
+ _monitor = MonitorUI()
92
255
 
93
256
  return _monitor
94
257
 
95
258
 
96
259
  if __name__ == "__main__":
260
+ # Example usage and testing code
97
261
  import time
98
-
99
262
  import numpy as np
100
263
 
264
+ # Example 1: Using Monitor directly
101
265
  for i in range(3):
102
266
  index = 0
103
267
  while True:
@@ -113,3 +277,29 @@ if __name__ == "__main__":
113
277
  m.add_point(index, np.random.randn(), np.sin(index / 20))
114
278
  index += 1
115
279
  time.sleep(0.2)
280
+
281
+ # Example 2: Using MonitorServer and Monitor
282
+ def run_server():
283
+ server = MonitorServer("127.0.0.1", 5555)
284
+ try:
285
+ while True:
286
+ time.sleep(1)
287
+ except KeyboardInterrupt:
288
+ pass
289
+
290
+ def run_client():
291
+ client = Monitor("127.0.0.1", 5555)
292
+ time.sleep(1) # Wait for server to start
293
+
294
+ client.set_column_names("index", "H", "S")
295
+ client.set_plots("(index,H);(index,S)")
296
+ client.roll()
297
+
298
+ for i in range(100):
299
+ client.add_point(i, np.random.randn(), np.sin(i / 20))
300
+ time.sleep(0.2)
301
+
302
+ if len(sys.argv) > 1 and sys.argv[1] == "server":
303
+ run_server()
304
+ elif len(sys.argv) > 1 and sys.argv[1] == "client":
305
+ run_client()
qulab/monitor/ploter.py CHANGED
@@ -1,123 +1,148 @@
1
- from .config import COL_SEL, COL_UNSEL, SymSize, defualt_colors, ridx, widths
1
+ """
2
+ QuLab Monitor Plotter Module
3
+
4
+ This module provides a custom plotting widget based on pyqtgraph for real-time
5
+ data visualization. It includes features like auto-ranging, mouse interaction
6
+ for data selection, and clipboard integration.
7
+ """
8
+
9
+ from .config import (COLOR_SELECTED, COLOR_UNSELECTED, SYMBOL_SIZES,
10
+ DEFAULT_COLORS, ROLL_INDICES, LINE_WIDTHS)
2
11
  from .qt_compat import QtWidgets
3
12
 
4
13
  # the plotting widget
5
14
  try:
6
15
  import pyqtgraph as pg
7
- except:
16
+ except ImportError:
8
17
  raise ImportError("Please install pyqtgraph first")
9
18
 
10
19
  try:
11
20
  import pyperclip as pc
12
- hasCliper = True
13
- except:
14
- hasCliper = False
21
+ HAS_CLIPBOARD = True
22
+ except ImportError:
23
+ HAS_CLIPBOARD = False
15
24
 
16
25
 
17
26
  class PlotWidget(pg.PlotWidget):
27
+ """
28
+ Custom plotting widget extending pyqtgraph's PlotWidget.
29
+
30
+ This widget provides additional features like:
31
+ - Configurable plot colors and styles
32
+ - Mouse interaction for data point selection
33
+ - Clipboard integration for selected data points
34
+ - Auto-ranging and axis control
35
+
36
+ Args:
37
+ minimum_height: Minimum height of the plot widget in pixels
38
+ colors: List of RGB color tuples for plot lines
39
+ """
18
40
 
19
41
  def __init__(self, minimum_height=300, colors=None):
20
- self.XAxisLinked = False
21
- self.YAxisLinked = False
42
+ self.x_axis_linked = False
43
+ self.y_axis_linked = False
22
44
  if colors is None:
23
- colors = defualt_colors
24
- elif len(colors) < len(defualt_colors):
25
- colors.extend(defualt_colors[len(colors):])
45
+ colors = DEFAULT_COLORS
46
+ elif len(colors) < len(DEFAULT_COLORS):
47
+ colors.extend(DEFAULT_COLORS[len(colors):])
26
48
  self.colors = colors
27
- self.xname = ""
28
- self.yname = ""
49
+ self.x_name = ""
50
+ self.y_name = ""
29
51
  super().__init__()
30
52
 
31
53
  self.setMinimumHeight(minimum_height)
32
54
  self.showGrid(x=True, y=True)
33
- self.setBackground(COL_UNSEL)
55
+ self.setBackground(COLOR_UNSELECTED)
34
56
 
35
57
  self.plotItem.vb.autoRange()
36
58
 
37
59
  ## Labeling
38
- self.XLabel = QtWidgets.QLabel(self)
39
- self.XLabel.setText("X:")
40
- self.XLabel.move(0, 5)
41
- self.YLabel = QtWidgets.QLabel(self)
42
- self.YLabel.setText("Y:")
43
- self.YLabel.move(0, 35)
60
+ self.x_label = QtWidgets.QLabel(self)
61
+ self.x_label.setText("X:")
62
+ self.x_label.move(0, 5)
63
+ self.y_label = QtWidgets.QLabel(self)
64
+ self.y_label.setText("Y:")
65
+ self.y_label.move(0, 35)
44
66
 
45
67
  self.plots = {}
46
- self.clippos1 = 0
47
- self.clippos2 = 0
68
+ self.clip_pos_start = 0
69
+ self.clip_pos_end = 0
48
70
  self.range_select = False
49
- for i in ridx:
50
- self.plots[i] = \
51
- self.plot([],[] ,pen={"color":self.colors[i] ,"width":widths[i]} ,
52
- symbolBrush = self.colors[i],
53
- symbolPen = { "width":0 ,"color":self.colors[i] } ,
54
- symbolSize =SymSize[i] ,
71
+ for idx in ROLL_INDICES:
72
+ self.plots[idx] = \
73
+ self.plot([],[] ,pen={"color":self.colors[idx] ,"width":LINE_WIDTHS[idx]} ,
74
+ symbolBrush = self.colors[idx],
75
+ symbolPen = { "width":0 ,"color":self.colors[idx] } ,
76
+ symbolSize =SYMBOL_SIZES[idx] ,
55
77
  )
56
78
  self.update()
57
79
 
58
- def set_X_label(self, w):
59
- self.xname = w
60
- self.XLabel.setText(f"X:{w}")
80
+ def set_x_label(self, label: str) -> None:
81
+ """Set the X-axis label."""
82
+ self.x_name = label
83
+ self.x_label.setText(f"X:{label}")
61
84
 
62
- def set_Y_label(self, w):
63
- self.yname = w
64
- self.YLabel.setText(f"Y:{w}")
85
+ def set_y_label(self, label: str) -> None:
86
+ """Set the Y-axis label."""
87
+ self.y_name = label
88
+ self.y_label.setText(f"Y:{label}")
65
89
 
66
- def auto_range(self):
90
+ def auto_range(self) -> None:
91
+ """Automatically adjust plot range to show all data."""
67
92
  self.plotItem.vb.autoRange()
68
93
 
69
- def enable_auto_range(self):
94
+ def enable_auto_range(self) -> None:
95
+ """Enable automatic range adjustment."""
70
96
  self.plotItem.vb.enableAutoRange()
71
97
 
72
- def keyPressEvent(self, ev):
73
- #print(ev.text());
74
- tx = ev.text()
75
- if ('f' == tx or 'F' == tx):
98
+ def keyPressEvent(self, event):
99
+ """Handle keyboard events for plot control.
100
+
101
+ Keys:
102
+ F/f: Auto-range the plot
103
+ A/a: Enable auto-pan
104
+ R/r: Enable range selection mode
105
+ """
106
+ key = event.text().lower()
107
+ if key == 'f':
76
108
  self.plotItem.vb.autoRange()
77
- if ('a' == tx or 'A' == tx):
109
+ elif key == 'a':
78
110
  self.plotItem.vb.setAutoPan()
79
- if ('r' == tx or 'R' == tx):
111
+ elif key == 'r':
80
112
  self.range_select = True
81
- super().keyPressEvent(ev)
113
+ super().keyPressEvent(event)
82
114
 
83
- def keyReleaseEvent(self, ev):
84
- #print(ev.text());
85
- tx = ev.text()
86
- if ('f' == tx or 'F' == tx):
87
- self.plotItem.vb.autoRange()
88
- if ('a' == tx or 'A' == tx):
89
- self.plotItem.vb.setAutoPan()
90
- if ('r' == tx or 'R' == tx):
115
+ def keyReleaseEvent(self, event):
116
+ """Handle key release events."""
117
+ key = event.text().lower()
118
+ if key == 'r':
91
119
  self.range_select = False
92
- super().keyReleaseEvent(ev)
93
-
94
- def mousePressEvent(self, ev):
95
- if (4 == ev.button()):
96
- # print(ev.flags())
97
- if (hasCliper):
98
- # print("Mouse is pressed")
99
- self.clippos1 = self.plotItem.vb.mapSceneToView(ev.pos()).x()
120
+ super().keyReleaseEvent(event)
121
+
122
+ def mousePressEvent(self, event):
123
+ """Handle mouse press events for data selection."""
124
+ if event.button() == 4 and HAS_CLIPBOARD:
125
+ self.clip_pos_start = self.plotItem.vb.mapSceneToView(event.pos()).x()
100
126
  else:
101
- super().mousePressEvent(ev)
102
-
103
- def mouseReleaseEvent(self, ev):
104
- if (4 == ev.button()):
105
- p = ev.pos()
106
- if (hasCliper):
107
- # print("Mouse is released")
108
- self.clippos2 = self.plotItem.vb.mapSceneToView(ev.pos()).x()
109
- if (self.range_select):
110
- pc.copy(f"{self.clippos1},{self.clippos2}")
111
- else:
112
- pc.copy(self.clippos2)
127
+ super().mousePressEvent(event)
128
+
129
+ def mouseReleaseEvent(self, event):
130
+ """Handle mouse release events and copy selected data to clipboard."""
131
+ if event.button() == 4 and HAS_CLIPBOARD:
132
+ self.clip_pos_end = self.plotItem.vb.mapSceneToView(event.pos()).x()
133
+ if self.range_select:
134
+ pc.copy(f"{self.clip_pos_start},{self.clip_pos_end}")
135
+ else:
136
+ pc.copy(str(self.clip_pos_end))
113
137
  else:
114
- super().mouseReleaseEvent(ev)
138
+ super().mouseReleaseEvent(event)
115
139
 
116
140
  def update(self):
117
141
  super().update()
118
142
 
119
- def set_data(self, i, x, y):
120
- self.plots[i].setData(x, y)
143
+ def set_data(self, index: int, x_data, y_data) -> None:
144
+ """Update plot data for the specified index."""
145
+ self.plots[index].setData(x_data, y_data)
121
146
 
122
147
  # def mouseDoubleClickEvent(self, ev):
123
148
  # super().mouseDoubleClickEvent(ev)
@@ -1,16 +1,45 @@
1
+ """
2
+ QuLab Monitor Qt Compatibility Module
3
+
4
+ This module provides compatibility layer for Qt constants and enums across
5
+ different Qt bindings (PyQt5, PyQt6, PySide2, PySide6). It ensures consistent
6
+ access to Qt constants regardless of the Qt binding being used.
7
+
8
+ The module exports the following constants:
9
+ - AlignRight: Right alignment flag
10
+ - BottomDockWidgetArea: Bottom dock widget area constant
11
+ - ScrollBarAlwaysOn: Always show scrollbar policy
12
+ - ScrollBarAlwaysOff: Never show scrollbar policy
13
+ - TopDockWidgetArea: Top dock widget area constant
14
+ """
15
+
1
16
  from matplotlib.backends.qt_compat import QT_API, QtCore, QtWidgets
2
17
 
18
+ # Define Qt constants based on the Qt binding being used
3
19
  if QT_API in ['PySide6', 'PyQt6']:
20
+ # Qt6 uses enum flags
4
21
  AlignRight = QtCore.Qt.AlignmentFlag.AlignRight
5
22
  BottomDockWidgetArea = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea
6
23
  ScrollBarAlwaysOn = QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn
7
24
  ScrollBarAlwaysOff = QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
8
25
  TopDockWidgetArea = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
9
26
  elif QT_API in ['PyQt5', 'PySide2']:
27
+ # Qt5 uses direct constants
10
28
  AlignRight = QtCore.Qt.AlignRight
11
29
  BottomDockWidgetArea = QtCore.Qt.BottomDockWidgetArea
12
30
  ScrollBarAlwaysOn = QtCore.Qt.ScrollBarAlwaysOn
13
31
  ScrollBarAlwaysOff = QtCore.Qt.ScrollBarAlwaysOff
14
32
  TopDockWidgetArea = QtCore.Qt.TopDockWidgetArea
15
33
  else:
16
- raise AssertionError(f"Unexpected QT_API: {QT_API}")
34
+ raise ValueError(f"Unsupported Qt binding: {QT_API}")
35
+
36
+ # Export all constants
37
+ __all__ = [
38
+ 'QtCore',
39
+ 'QtWidgets',
40
+ 'AlignRight',
41
+ 'BottomDockWidgetArea',
42
+ 'ScrollBarAlwaysOn',
43
+ 'ScrollBarAlwaysOff',
44
+ 'TopDockWidgetArea',
45
+ ]