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/dataset.py
CHANGED
@@ -1,77 +1,184 @@
|
|
1
|
+
"""
|
2
|
+
QuLab Monitor Dataset Module
|
3
|
+
|
4
|
+
This module provides data management functionality for the QuLab monitor application.
|
5
|
+
It handles storage and retrieval of time-series data with support for both point-by-point
|
6
|
+
and trace-based data collection.
|
7
|
+
|
8
|
+
The Dataset class maintains a rolling buffer of data frames, where each frame can
|
9
|
+
contain multiple named columns of numerical data.
|
10
|
+
"""
|
11
|
+
|
1
12
|
from collections import defaultdict, deque
|
13
|
+
from typing import Union, Sequence
|
2
14
|
|
3
|
-
from .config import
|
15
|
+
from .config import ROLL_BUFFER_SIZE
|
4
16
|
|
5
17
|
PAUSE_TIME = 1
|
6
18
|
|
7
|
-
Number = int
|
19
|
+
Number = Union[int, float, complex]
|
20
|
+
DataPoint = Union[Number, Sequence[Number]]
|
21
|
+
DataFrame = Union[Sequence[Number], Sequence[Sequence[Number]]]
|
8
22
|
|
9
23
|
|
10
24
|
def remove_duplicates(input_list: list[str]) -> list[str]:
|
11
25
|
"""
|
12
|
-
Remove duplicates from a list of strings
|
26
|
+
Remove duplicates from a list of strings while preserving order.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
input_list: List of strings that may contain duplicates
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
List of unique strings in their original order
|
13
33
|
"""
|
14
34
|
return list(dict.fromkeys(input_list))
|
15
35
|
|
16
36
|
|
17
|
-
class Dataset
|
37
|
+
class Dataset:
|
38
|
+
"""
|
39
|
+
A data management class for storing and retrieving time-series data.
|
18
40
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
self.dirty = True
|
41
|
+
This class maintains a rolling buffer of data frames, where each frame
|
42
|
+
contains multiple columns of numerical data. It supports both point-by-point
|
43
|
+
data collection and trace-based data collection.
|
23
44
|
|
24
|
-
|
25
|
-
|
45
|
+
Attributes:
|
46
|
+
column_names: List of column names for the dataset
|
47
|
+
box: Rolling buffer containing data frames
|
48
|
+
dirty: Flag indicating if data has been modified since last read
|
49
|
+
"""
|
26
50
|
|
27
|
-
def
|
28
|
-
|
51
|
+
def __init__(self):
|
52
|
+
"""Initialize an empty dataset with a rolling buffer."""
|
53
|
+
self.column_names: list[str] = []
|
54
|
+
self.box: deque[dict] = deque(maxlen=ROLL_BUFFER_SIZE)
|
55
|
+
self.dirty: bool = True
|
56
|
+
|
57
|
+
def clear(self) -> None:
|
58
|
+
"""Clear all data from the dataset."""
|
29
59
|
self.box.clear()
|
30
|
-
self.box.appendleft(o)
|
31
60
|
|
32
|
-
def
|
61
|
+
def clear_history(self) -> None:
|
62
|
+
"""
|
63
|
+
Clear historical data while preserving the most recent frame.
|
64
|
+
|
65
|
+
This is useful for maintaining the current state while discarding
|
66
|
+
historical data that is no longer needed.
|
67
|
+
"""
|
68
|
+
if self.box:
|
69
|
+
current_frame = self.box.popleft()
|
70
|
+
self.box.clear()
|
71
|
+
self.box.appendleft(current_frame)
|
72
|
+
|
73
|
+
def set_column_names(self, column_names: list[str]) -> None:
|
74
|
+
"""
|
75
|
+
Set or update the column names for the dataset.
|
76
|
+
|
77
|
+
If the new column names differ from the current ones, all existing
|
78
|
+
data will be cleared to maintain consistency.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
column_names: List of unique column names
|
82
|
+
"""
|
33
83
|
column_names = remove_duplicates(column_names)
|
34
84
|
if column_names != self.column_names:
|
35
85
|
self.clear()
|
36
86
|
self.column_names = column_names
|
37
87
|
|
38
|
-
def get_data(self, step: int,
|
39
|
-
|
88
|
+
def get_data(self, step: int, x_name: str,
|
89
|
+
y_name: str) -> tuple[list[Number], list[Number]]:
|
90
|
+
"""
|
91
|
+
Retrieve X-Y data pairs from a specific step in the dataset.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
step: Index of the data frame to retrieve
|
95
|
+
x_name: Name of the column to use for X values
|
96
|
+
y_name: Name of the column to use for Y values
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Tuple containing lists of X and Y values. Returns empty lists if
|
100
|
+
the requested step is not available.
|
101
|
+
"""
|
40
102
|
try:
|
41
|
-
|
103
|
+
frame = self.box[step]
|
104
|
+
return frame[x_name], frame[y_name]
|
42
105
|
except IndexError:
|
43
106
|
return [], []
|
44
|
-
return b[xname], b[yname]
|
45
107
|
|
46
|
-
def append(self, dataframe:
|
108
|
+
def append(self, dataframe: DataFrame) -> None:
|
109
|
+
"""
|
110
|
+
Append new data to the dataset.
|
111
|
+
|
112
|
+
This method automatically determines whether the input is point data
|
113
|
+
or trace data based on its structure.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
dataframe: Either a list of values for point data or a list of
|
117
|
+
lists for trace data. The length must match the number
|
118
|
+
of columns.
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
AssertionError: If the input data length doesn't match the number
|
122
|
+
of columns.
|
123
|
+
TypeError: If the input data format is invalid.
|
124
|
+
"""
|
47
125
|
if not dataframe:
|
48
126
|
return
|
49
127
|
try:
|
50
|
-
|
128
|
+
# Test if dataframe is a list of lists (trace data)
|
129
|
+
iter(dataframe[0])
|
51
130
|
self._append_traces(dataframe)
|
52
131
|
except TypeError:
|
53
132
|
self._append_points(dataframe)
|
54
133
|
|
55
|
-
def roll(self):
|
134
|
+
def roll(self) -> None:
|
135
|
+
"""
|
136
|
+
Add a new empty frame to the dataset.
|
137
|
+
|
138
|
+
This creates a new defaultdict that will automatically create empty
|
139
|
+
lists for any new column names accessed.
|
140
|
+
"""
|
56
141
|
self.box.appendleft(defaultdict(list))
|
57
142
|
|
58
|
-
def _append_points(self, points:
|
143
|
+
def _append_points(self, points: Sequence[Number]) -> None:
|
144
|
+
"""
|
145
|
+
Append point data to the current frame.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
points: List of values, one for each column
|
149
|
+
|
150
|
+
Raises:
|
151
|
+
AssertionError: If the number of points doesn't match the number
|
152
|
+
of columns
|
153
|
+
"""
|
59
154
|
self.dirty = True
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
155
|
+
if len(points) != len(self.column_names):
|
156
|
+
raise AssertionError(
|
157
|
+
"Point data length mismatch\n"
|
158
|
+
f"Expected {len(self.column_names)} values for columns: {self.column_names}\n"
|
159
|
+
f"Received {len(points)} values: {points}"
|
160
|
+
)
|
161
|
+
|
162
|
+
for name, value in zip(self.column_names, points):
|
163
|
+
self.box[0][name].append(value)
|
164
|
+
|
165
|
+
def _append_traces(self, traces: Sequence[Sequence[Number]]) -> None:
|
166
|
+
"""
|
167
|
+
Append trace data as a new frame.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
traces: List of data sequences, one for each column
|
171
|
+
|
172
|
+
Raises:
|
173
|
+
AssertionError: If the number of traces doesn't match the number
|
174
|
+
of columns
|
175
|
+
"""
|
70
176
|
self.dirty = True
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
177
|
+
if len(traces) != len(self.column_names):
|
178
|
+
raise AssertionError(
|
179
|
+
"Trace data length mismatch\n"
|
180
|
+
f"Expected {len(self.column_names)} sequences for columns: {self.column_names}\n"
|
181
|
+
f"Received {len(traces)} sequences"
|
182
|
+
)
|
183
|
+
|
77
184
|
self.box.appendleft(dict(zip(self.column_names, traces)))
|
qulab/monitor/event_queue.py
CHANGED
@@ -1,54 +1,127 @@
|
|
1
|
+
"""
|
2
|
+
QuLab Monitor Event Queue Module
|
3
|
+
|
4
|
+
This module implements an event handling system for the QuLab monitor application.
|
5
|
+
It processes commands and data received through a multiprocessing queue to update
|
6
|
+
the monitor's display and datasets.
|
7
|
+
|
8
|
+
The event queue handles various types of commands:
|
9
|
+
- PN/TN: Set column names for point/trace data
|
10
|
+
- PD/TD: Append point/trace data
|
11
|
+
- ROLL: Create new data frame
|
12
|
+
- PXY/TXY: Update plot configurations
|
13
|
+
"""
|
14
|
+
|
1
15
|
import warnings
|
2
16
|
from multiprocessing import Queue
|
17
|
+
from typing import Any, Literal, Tuple, Union
|
3
18
|
|
4
19
|
from .dataset import Dataset
|
5
20
|
from .toolbar import ToolBar
|
6
21
|
|
22
|
+
# Type definitions
|
23
|
+
CommandType = Literal["PN", "TN", "PD", "TD", "ROLL", "PXY", "TXY"]
|
24
|
+
DataType = Union[list[str], list[float], str, None]
|
25
|
+
EventType = Tuple[CommandType, DataType]
|
26
|
+
|
27
|
+
|
28
|
+
class EventQueue:
|
29
|
+
"""
|
30
|
+
Event handler for processing monitor commands and data.
|
7
31
|
|
8
|
-
class
|
32
|
+
This class manages the communication between the data collection process
|
33
|
+
and the monitor display. It receives commands and data through a queue
|
34
|
+
and updates the appropriate components of the monitor.
|
35
|
+
|
36
|
+
Attributes:
|
37
|
+
toolbar: The monitor's toolbar for UI controls
|
38
|
+
queue: Multiprocessing queue for receiving commands and data
|
39
|
+
point_dataset: Dataset for point-by-point data collection
|
40
|
+
trace_dataset: Dataset for trace-based data collection
|
41
|
+
"""
|
9
42
|
|
10
43
|
def __init__(self, queue: Queue, toolbar: ToolBar, point_dataset: Dataset,
|
11
44
|
trace_dataset: Dataset):
|
45
|
+
"""
|
46
|
+
Initialize the event queue.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
queue: Multiprocessing queue for receiving commands
|
50
|
+
toolbar: Monitor's toolbar instance
|
51
|
+
point_dataset: Dataset for point data
|
52
|
+
trace_dataset: Dataset for trace data
|
53
|
+
"""
|
12
54
|
self.toolbar = toolbar
|
13
55
|
self.queue = queue
|
14
56
|
self.point_dataset = point_dataset
|
15
57
|
self.trace_dataset = trace_dataset
|
16
58
|
|
17
|
-
def flush(self):
|
18
|
-
|
19
|
-
|
59
|
+
def flush(self) -> None:
|
60
|
+
"""
|
61
|
+
Process all pending events in the queue.
|
62
|
+
|
63
|
+
This method retrieves and processes all available events from the queue.
|
64
|
+
Each event should be a tuple containing a command string and associated data.
|
65
|
+
Invalid events are logged as warnings and skipped.
|
66
|
+
"""
|
67
|
+
while not self.queue.empty():
|
20
68
|
try:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
69
|
+
event = self.queue.get()
|
70
|
+
if not isinstance(event, tuple):
|
71
|
+
raise ValueError("Queue event must be a tuple")
|
72
|
+
if len(event) != 2:
|
73
|
+
raise ValueError("Queue event must contain exactly two elements (command, data)")
|
74
|
+
|
75
|
+
command, data = event
|
76
|
+
if not isinstance(command, str):
|
77
|
+
raise ValueError("Command must be a string")
|
78
|
+
|
79
|
+
self.handle(command, data)
|
80
|
+
|
81
|
+
except (ValueError, AssertionError) as error:
|
82
|
+
warnings.warn(f"Invalid event format: {error}")
|
31
83
|
continue
|
32
84
|
|
33
|
-
|
85
|
+
def handle(self, command: str, data: Any) -> None:
|
86
|
+
"""
|
87
|
+
Process a single event based on its command type.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
command: String identifying the type of event
|
91
|
+
data: Data associated with the event
|
34
92
|
|
35
|
-
|
36
|
-
|
37
|
-
|
93
|
+
The following commands are supported:
|
94
|
+
PN: Set point data column names
|
95
|
+
TN: Set trace data column names
|
96
|
+
PD: Append point data
|
97
|
+
TD: Append trace data
|
98
|
+
ROLL: Create new data frame
|
99
|
+
PXY: Update point plot configuration
|
100
|
+
TXY: Update trace plot configuration
|
101
|
+
"""
|
102
|
+
match command:
|
103
|
+
case "PN": # Set point data column names
|
38
104
|
self.point_dataset.set_column_names(data)
|
39
105
|
self.toolbar.refresh_comb()
|
40
|
-
|
106
|
+
|
107
|
+
case "TN": # Set trace data column names
|
41
108
|
self.trace_dataset.set_column_names(data)
|
42
109
|
self.toolbar.refresh_comb()
|
43
|
-
|
110
|
+
|
111
|
+
case "PD": # Append point data
|
44
112
|
self.point_dataset.append(data)
|
45
|
-
|
113
|
+
|
114
|
+
case "TD": # Append trace data
|
46
115
|
self.trace_dataset.append(data)
|
47
|
-
|
116
|
+
|
117
|
+
case "ROLL": # Create new data frame
|
48
118
|
self.point_dataset.roll()
|
49
|
-
|
119
|
+
|
120
|
+
case "PXY": # Update point plot configuration
|
50
121
|
self.toolbar.set_point_text(data)
|
51
|
-
|
122
|
+
|
123
|
+
case "TXY": # Update trace plot configuration
|
52
124
|
self.toolbar.set_trace_text(data)
|
125
|
+
|
53
126
|
case _:
|
54
|
-
warnings.warn(f"
|
127
|
+
warnings.warn(f"Unknown command: {command}")
|