QuLab 2.0.2__cp310-cp310-macosx_10_9_universal2.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-2.0.2.dist-info/LICENSE +21 -0
- QuLab-2.0.2.dist-info/METADATA +98 -0
- QuLab-2.0.2.dist-info/RECORD +84 -0
- QuLab-2.0.2.dist-info/WHEEL +5 -0
- QuLab-2.0.2.dist-info/entry_points.txt +2 -0
- QuLab-2.0.2.dist-info/top_level.txt +1 -0
- qulab/__init__.py +1 -0
- qulab/__main__.py +26 -0
- qulab/fun.cpython-310-darwin.so +0 -0
- qulab/monitor/__init__.py +1 -0
- qulab/monitor/__main__.py +8 -0
- qulab/monitor/config.py +41 -0
- qulab/monitor/dataset.py +77 -0
- qulab/monitor/event_queue.py +54 -0
- qulab/monitor/mainwindow.py +234 -0
- qulab/monitor/monitor.py +93 -0
- qulab/monitor/ploter.py +123 -0
- qulab/monitor/qt_compat.py +16 -0
- qulab/monitor/toolbar.py +265 -0
- qulab/scan/__init__.py +3 -0
- qulab/scan/curd.py +144 -0
- qulab/scan/expression.py +505 -0
- qulab/scan/models.py +540 -0
- qulab/scan/optimize.py +69 -0
- qulab/scan/query_record.py +361 -0
- qulab/scan/recorder.py +447 -0
- qulab/scan/scan.py +701 -0
- qulab/scan/utils.py +37 -0
- qulab/storage/__init__.py +0 -0
- qulab/storage/__main__.py +51 -0
- qulab/storage/backend/__init__.py +0 -0
- qulab/storage/backend/redis.py +204 -0
- qulab/storage/base_dataset.py +352 -0
- qulab/storage/chunk.py +60 -0
- qulab/storage/dataset.py +127 -0
- qulab/storage/file.py +273 -0
- qulab/storage/models/__init__.py +22 -0
- qulab/storage/models/base.py +4 -0
- qulab/storage/models/config.py +28 -0
- qulab/storage/models/file.py +89 -0
- qulab/storage/models/ipy.py +58 -0
- qulab/storage/models/models.py +88 -0
- qulab/storage/models/record.py +161 -0
- qulab/storage/models/report.py +22 -0
- qulab/storage/models/tag.py +93 -0
- qulab/storage/storage.py +95 -0
- qulab/sys/__init__.py +0 -0
- qulab/sys/chat.py +688 -0
- qulab/sys/device/__init__.py +3 -0
- qulab/sys/device/basedevice.py +221 -0
- qulab/sys/device/loader.py +86 -0
- qulab/sys/device/utils.py +46 -0
- qulab/sys/drivers/FakeInstrument.py +52 -0
- qulab/sys/drivers/__init__.py +0 -0
- qulab/sys/ipy_events.py +125 -0
- qulab/sys/net/__init__.py +0 -0
- qulab/sys/net/bencoder.py +205 -0
- qulab/sys/net/cli.py +169 -0
- qulab/sys/net/dhcp.py +543 -0
- qulab/sys/net/dhcpd.py +176 -0
- qulab/sys/net/kad.py +1142 -0
- qulab/sys/net/kcp.py +192 -0
- qulab/sys/net/nginx.py +192 -0
- qulab/sys/progress.py +190 -0
- qulab/sys/rpc/__init__.py +0 -0
- qulab/sys/rpc/client.py +0 -0
- qulab/sys/rpc/exceptions.py +96 -0
- qulab/sys/rpc/msgpack.py +1052 -0
- qulab/sys/rpc/msgpack.pyi +41 -0
- qulab/sys/rpc/rpc.py +412 -0
- qulab/sys/rpc/serialize.py +139 -0
- qulab/sys/rpc/server.py +29 -0
- qulab/sys/rpc/socket.py +29 -0
- qulab/sys/rpc/utils.py +25 -0
- qulab/sys/rpc/worker.py +0 -0
- qulab/sys/rpc/zmq_socket.py +209 -0
- qulab/version.py +1 -0
- qulab/visualization/__init__.py +188 -0
- qulab/visualization/__main__.py +71 -0
- qulab/visualization/_autoplot.py +463 -0
- qulab/visualization/plot_layout.py +408 -0
- qulab/visualization/plot_seq.py +90 -0
- qulab/visualization/qdat.py +152 -0
- qulab/visualization/widgets.py +86 -0
qulab/monitor/monitor.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# try:
|
|
5
|
+
# mp.set_start_method("spawn")
|
|
6
|
+
# except:
|
|
7
|
+
# pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(queue: mp.Queue,
|
|
11
|
+
ncols: int = 4,
|
|
12
|
+
minimum_height: int = 400,
|
|
13
|
+
colors: list[tuple[int, int, int]] = []):
|
|
14
|
+
from .mainwindow import MainWindow
|
|
15
|
+
from .qt_compat import QtWidgets
|
|
16
|
+
|
|
17
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
18
|
+
main = MainWindow(queue, ncols, minimum_height, colors)
|
|
19
|
+
sys.exit(app.exec())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Monitor():
|
|
23
|
+
|
|
24
|
+
def __init__(self,
|
|
25
|
+
number_of_columns: int = 4,
|
|
26
|
+
minimum_height: int = 400,
|
|
27
|
+
colors: list[tuple[int, int, int]] = []):
|
|
28
|
+
self.colors = [tuple(color) for color in colors]
|
|
29
|
+
self.number_of_columns = number_of_columns
|
|
30
|
+
self.minimum_height = minimum_height
|
|
31
|
+
self.queue = mp.Queue(20)
|
|
32
|
+
self.process = None
|
|
33
|
+
self.start()
|
|
34
|
+
|
|
35
|
+
def start(self):
|
|
36
|
+
if self.process is not None and self.process.is_alive():
|
|
37
|
+
return
|
|
38
|
+
self.queue = mp.Queue(20)
|
|
39
|
+
self.process = mp.Process(target=main,
|
|
40
|
+
args=(self.queue, self.number_of_columns,
|
|
41
|
+
self.minimum_height, self.colors))
|
|
42
|
+
self.process.start()
|
|
43
|
+
|
|
44
|
+
def _put(self, w: tuple):
|
|
45
|
+
self.queue.put(w)
|
|
46
|
+
|
|
47
|
+
def roll(self):
|
|
48
|
+
self._put(("ROLL", None))
|
|
49
|
+
|
|
50
|
+
def set_column_names(self, *arg):
|
|
51
|
+
self._put(('PN', list(arg)))
|
|
52
|
+
|
|
53
|
+
def add_point(self, *arg):
|
|
54
|
+
self._put(('PD', list(arg)))
|
|
55
|
+
|
|
56
|
+
def set_plots(self, arg):
|
|
57
|
+
"""
|
|
58
|
+
arg: str, like "(x,y)" or "(x1,y1);(x2,y2);"
|
|
59
|
+
"""
|
|
60
|
+
self._put(('PXY', str(arg)))
|
|
61
|
+
|
|
62
|
+
def set_trace_column_names(self, *arg):
|
|
63
|
+
self._put(('TN', list(arg)))
|
|
64
|
+
|
|
65
|
+
def add_trace(self, *arg):
|
|
66
|
+
self._put(('TD', list(arg)))
|
|
67
|
+
|
|
68
|
+
def set_trace_plots(self, arg):
|
|
69
|
+
"""
|
|
70
|
+
arg: str, like "(x,y)" or "(x1,y1);(x2,y2);"
|
|
71
|
+
"""
|
|
72
|
+
self._put(('TXY', str(arg)))
|
|
73
|
+
|
|
74
|
+
def __del__(self):
|
|
75
|
+
try:
|
|
76
|
+
self.process.kill()
|
|
77
|
+
except:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def is_alive(self):
|
|
81
|
+
return self.process.is_alive()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_monitor = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_monitor(auto_open=True):
|
|
88
|
+
global _monitor
|
|
89
|
+
|
|
90
|
+
if auto_open and (_monitor is None or not _monitor.is_alive()):
|
|
91
|
+
_monitor = Monitor()
|
|
92
|
+
|
|
93
|
+
return _monitor
|
qulab/monitor/ploter.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from .config import COL_SEL, COL_UNSEL, SymSize, defualt_colors, ridx, widths
|
|
2
|
+
from .qt_compat import QtWidgets
|
|
3
|
+
|
|
4
|
+
# the plotting widget
|
|
5
|
+
try:
|
|
6
|
+
import pyqtgraph as pg
|
|
7
|
+
except:
|
|
8
|
+
raise ImportError("Please install pyqtgraph first")
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import pyperclip as pc
|
|
12
|
+
hasCliper = True
|
|
13
|
+
except:
|
|
14
|
+
hasCliper = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlotWidget(pg.PlotWidget):
|
|
18
|
+
|
|
19
|
+
def __init__(self, minimum_height=300, colors=None):
|
|
20
|
+
self.XAxisLinked = False
|
|
21
|
+
self.YAxisLinked = False
|
|
22
|
+
if colors is None:
|
|
23
|
+
colors = defualt_colors
|
|
24
|
+
elif len(colors) < len(defualt_colors):
|
|
25
|
+
colors.extend(defualt_colors[len(colors):])
|
|
26
|
+
self.colors = colors
|
|
27
|
+
self.xname = ""
|
|
28
|
+
self.yname = ""
|
|
29
|
+
super().__init__()
|
|
30
|
+
|
|
31
|
+
self.setMinimumHeight(minimum_height)
|
|
32
|
+
self.showGrid(x=True, y=True)
|
|
33
|
+
self.setBackground(COL_UNSEL)
|
|
34
|
+
|
|
35
|
+
self.plotItem.vb.autoRange()
|
|
36
|
+
|
|
37
|
+
## 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)
|
|
44
|
+
|
|
45
|
+
self.plots = {}
|
|
46
|
+
self.clippos1 = 0
|
|
47
|
+
self.clippos2 = 0
|
|
48
|
+
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] ,
|
|
55
|
+
)
|
|
56
|
+
self.update()
|
|
57
|
+
|
|
58
|
+
def set_X_label(self, w):
|
|
59
|
+
self.xname = w
|
|
60
|
+
self.XLabel.setText(f"X:{w}")
|
|
61
|
+
|
|
62
|
+
def set_Y_label(self, w):
|
|
63
|
+
self.yname = w
|
|
64
|
+
self.YLabel.setText(f"Y:{w}")
|
|
65
|
+
|
|
66
|
+
def auto_range(self):
|
|
67
|
+
self.plotItem.vb.autoRange()
|
|
68
|
+
|
|
69
|
+
def enable_auto_range(self):
|
|
70
|
+
self.plotItem.vb.enableAutoRange()
|
|
71
|
+
|
|
72
|
+
def keyPressEvent(self, ev):
|
|
73
|
+
#print(ev.text());
|
|
74
|
+
tx = ev.text()
|
|
75
|
+
if ('f' == tx or 'F' == tx):
|
|
76
|
+
self.plotItem.vb.autoRange()
|
|
77
|
+
if ('a' == tx or 'A' == tx):
|
|
78
|
+
self.plotItem.vb.setAutoPan()
|
|
79
|
+
if ('r' == tx or 'R' == tx):
|
|
80
|
+
self.range_select = True
|
|
81
|
+
super().keyPressEvent(ev)
|
|
82
|
+
|
|
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):
|
|
91
|
+
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()
|
|
100
|
+
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)
|
|
113
|
+
else:
|
|
114
|
+
super().mouseReleaseEvent(ev)
|
|
115
|
+
|
|
116
|
+
def update(self):
|
|
117
|
+
super().update()
|
|
118
|
+
|
|
119
|
+
def set_data(self, i, x, y):
|
|
120
|
+
self.plots[i].setData(x, y)
|
|
121
|
+
|
|
122
|
+
# def mouseDoubleClickEvent(self, ev):
|
|
123
|
+
# super().mouseDoubleClickEvent(ev)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from matplotlib.backends.qt_compat import QT_API, QtCore, QtWidgets
|
|
2
|
+
|
|
3
|
+
if QT_API in ['PySide6', 'PyQt6']:
|
|
4
|
+
AlignRight = QtCore.Qt.AlignmentFlag.AlignRight
|
|
5
|
+
BottomDockWidgetArea = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea
|
|
6
|
+
ScrollBarAlwaysOn = QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn
|
|
7
|
+
ScrollBarAlwaysOff = QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
|
8
|
+
TopDockWidgetArea = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
|
|
9
|
+
elif QT_API in ['PyQt5', 'PySide2']:
|
|
10
|
+
AlignRight = QtCore.Qt.AlignRight
|
|
11
|
+
BottomDockWidgetArea = QtCore.Qt.BottomDockWidgetArea
|
|
12
|
+
ScrollBarAlwaysOn = QtCore.Qt.ScrollBarAlwaysOn
|
|
13
|
+
ScrollBarAlwaysOff = QtCore.Qt.ScrollBarAlwaysOff
|
|
14
|
+
TopDockWidgetArea = QtCore.Qt.TopDockWidgetArea
|
|
15
|
+
else:
|
|
16
|
+
raise AssertionError(f"Unexpected QT_API: {QT_API}")
|
qulab/monitor/toolbar.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import re
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from .config import style
|
|
6
|
+
from .qt_compat import AlignRight, QtWidgets
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def matched_xy_pairs(patterns: str, lst: list[str]) -> list[tuple[str, str]]:
|
|
10
|
+
patterns = patterns.replace(" ", "").split(";")
|
|
11
|
+
pairs = []
|
|
12
|
+
for x, y in itertools.product(lst, repeat=2):
|
|
13
|
+
test = f"{x},{y}"
|
|
14
|
+
for pattern in patterns:
|
|
15
|
+
r = re.match(pattern, test)
|
|
16
|
+
if r and r.group(0) == test:
|
|
17
|
+
pairs.append((x, y))
|
|
18
|
+
break
|
|
19
|
+
return pairs
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FormatCombo(QtWidgets.QComboBox):
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.on_change_callable = None
|
|
27
|
+
|
|
28
|
+
def set_on_change_event_action(self, callback: Callable[[], None]):
|
|
29
|
+
self.on_change_callable = callback
|
|
30
|
+
self.activated.connect(callback)
|
|
31
|
+
|
|
32
|
+
def set_idx(self, idx: int):
|
|
33
|
+
self.setCurrentIndex(idx)
|
|
34
|
+
if (callable(self.on_change_callable)):
|
|
35
|
+
self.on_change_callable()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class XFormatCombo(FormatCombo):
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.addItem("real")
|
|
43
|
+
self.addItem("imag")
|
|
44
|
+
self.addItem("mag")
|
|
45
|
+
self.addItem("phase")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class YFormatCombo(FormatCombo):
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.addItem("mag")
|
|
53
|
+
self.addItem("phase")
|
|
54
|
+
self.addItem("real")
|
|
55
|
+
self.addItem("imag")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LineEdit(QtWidgets.QLineEdit):
|
|
59
|
+
|
|
60
|
+
def set_on_change_event_action(self, callback: Callable[[], None]):
|
|
61
|
+
self.on_change_callable = callback
|
|
62
|
+
self.editingFinished.connect(callback)
|
|
63
|
+
|
|
64
|
+
def set_text(self, w):
|
|
65
|
+
self.setText(w)
|
|
66
|
+
if (callable(self.on_change_callable)):
|
|
67
|
+
self.on_change_callable()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SelectionBundle():
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self.stx = LineEdit()
|
|
74
|
+
# select text
|
|
75
|
+
self.fx = XFormatCombo()
|
|
76
|
+
self.fy = YFormatCombo()
|
|
77
|
+
self.lx = QtWidgets.QCheckBox("logX")
|
|
78
|
+
self.ly = QtWidgets.QCheckBox("logY")
|
|
79
|
+
self.linkx = QtWidgets.QCheckBox("ShareX")
|
|
80
|
+
self.linky = QtWidgets.QCheckBox("ShareY")
|
|
81
|
+
self.sels = []
|
|
82
|
+
# tuple enumeration
|
|
83
|
+
|
|
84
|
+
def set_on_change_event_actions(self, on_text_edited, on_format_changed,
|
|
85
|
+
on_log_scale_marker_changed):
|
|
86
|
+
self.stx.set_on_change_event_action(on_text_edited)
|
|
87
|
+
self.fx.set_on_change_event_action(on_format_changed)
|
|
88
|
+
self.fy.set_on_change_event_action(on_format_changed)
|
|
89
|
+
self.lx.toggled.connect(on_format_changed)
|
|
90
|
+
self.ly.toggled.connect(on_format_changed)
|
|
91
|
+
self.linkx.toggled.connect(on_log_scale_marker_changed)
|
|
92
|
+
self.linky.toggled.connect(on_log_scale_marker_changed)
|
|
93
|
+
|
|
94
|
+
def rm4l(self): # remove from layout
|
|
95
|
+
self.stx.setParent(None)
|
|
96
|
+
self.fx.setParent(None)
|
|
97
|
+
self.fy.setParent(None)
|
|
98
|
+
self.lx.setParent(None)
|
|
99
|
+
self.ly.setParent(None)
|
|
100
|
+
self.linkx.setParent(None)
|
|
101
|
+
self.linky.setParent(None)
|
|
102
|
+
|
|
103
|
+
def a2l(self, layout): # add to layout
|
|
104
|
+
i = 3
|
|
105
|
+
layout.addWidget(self.stx, 0, i)
|
|
106
|
+
i += 2
|
|
107
|
+
layout.addWidget(self.fx, 0, i)
|
|
108
|
+
i += 2
|
|
109
|
+
layout.addWidget(self.fy, 0, i)
|
|
110
|
+
i += 2
|
|
111
|
+
layout.addWidget(self.lx, 0, 10)
|
|
112
|
+
layout.addWidget(self.ly, 0, 11)
|
|
113
|
+
layout.addWidget(self.linkx, 0, 12)
|
|
114
|
+
layout.addWidget(self.linky, 0, 13)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ToolBar(QtWidgets.QWidget):
|
|
118
|
+
|
|
119
|
+
def __init__(self):
|
|
120
|
+
super().__init__()
|
|
121
|
+
|
|
122
|
+
# the buttons
|
|
123
|
+
# button for points
|
|
124
|
+
self.mode = 'P'
|
|
125
|
+
self.setStyleSheet(style)
|
|
126
|
+
|
|
127
|
+
self.pb = QtWidgets.QRadioButton('Points')
|
|
128
|
+
self.pb.setChecked(True)
|
|
129
|
+
self.pb.toggled.connect(self.toggle_mode)
|
|
130
|
+
|
|
131
|
+
# button for Traces
|
|
132
|
+
self.tb = QtWidgets.QRadioButton('Traces')
|
|
133
|
+
self.tb.toggled.connect(self.toggle_mode)
|
|
134
|
+
|
|
135
|
+
# text labels
|
|
136
|
+
self.ytxt_lb = QtWidgets.QLabel("(X,Y)")
|
|
137
|
+
self.ytxt_lb.setAlignment(AlignRight)
|
|
138
|
+
self.fx_lb = QtWidgets.QLabel("fx")
|
|
139
|
+
self.fx_lb.setAlignment(AlignRight)
|
|
140
|
+
self.fy_lb = QtWidgets.QLabel("fy")
|
|
141
|
+
self.fy_lb.setAlignment(AlignRight)
|
|
142
|
+
|
|
143
|
+
# enumeration
|
|
144
|
+
ps = SelectionBundle()
|
|
145
|
+
ps.set_on_change_event_actions(self.textEdited, self.generateXYFM,
|
|
146
|
+
self.link_edited)
|
|
147
|
+
|
|
148
|
+
ts = SelectionBundle()
|
|
149
|
+
ts.set_on_change_event_actions(self.textEdited, self.generateXYFM,
|
|
150
|
+
self.link_edited)
|
|
151
|
+
|
|
152
|
+
# connections :
|
|
153
|
+
self.ps = ps
|
|
154
|
+
self.ts = ts
|
|
155
|
+
|
|
156
|
+
# plot format configures
|
|
157
|
+
self.xypairs = []
|
|
158
|
+
self.xypairs_dirty = True
|
|
159
|
+
self.fx = None
|
|
160
|
+
self.fy = None
|
|
161
|
+
self.xyfm_dirty = True
|
|
162
|
+
self.link_dirty = True
|
|
163
|
+
self.lx = False
|
|
164
|
+
self.ly = False
|
|
165
|
+
# setting layout
|
|
166
|
+
self.layout = QtWidgets.QGridLayout()
|
|
167
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
168
|
+
self.setLayout(self.layout)
|
|
169
|
+
|
|
170
|
+
self.layout.addWidget(self.pb, 0, 0)
|
|
171
|
+
self.layout.addWidget(self.tb, 0, 1)
|
|
172
|
+
|
|
173
|
+
self.layout.addWidget(self.ytxt_lb, 0, 2)
|
|
174
|
+
self.layout.addWidget(self.fx_lb, 0, 4)
|
|
175
|
+
self.layout.addWidget(self.fy_lb, 0, 6)
|
|
176
|
+
|
|
177
|
+
self.AR = QtWidgets.QPushButton("AR")
|
|
178
|
+
self.AR.setMaximumWidth(30)
|
|
179
|
+
self.AR.setToolTip("Auto Range")
|
|
180
|
+
self.CR = QtWidgets.QPushButton("CLR")
|
|
181
|
+
self.CR.setMaximumWidth(30)
|
|
182
|
+
self.CR.setToolTip("Clearing History Plots")
|
|
183
|
+
self.CR_flag = False
|
|
184
|
+
self.layout.addWidget(self.AR, 0, 8)
|
|
185
|
+
self.layout.addWidget(self.CR, 0, 9)
|
|
186
|
+
|
|
187
|
+
self.refresh_layout()
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def column_names(self) -> list[str]:
|
|
191
|
+
return {
|
|
192
|
+
"P": self.mainwindow.point_data_box.column_names,
|
|
193
|
+
"T": self.mainwindow.trace_data_box.column_names
|
|
194
|
+
}[self.mode]
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def selections(self) -> SelectionBundle:
|
|
198
|
+
return {"P": self.ps, "T": self.ts}[self.mode]
|
|
199
|
+
|
|
200
|
+
def set_trace_text(self, text: str):
|
|
201
|
+
self.ts.stx.set_text(text)
|
|
202
|
+
|
|
203
|
+
def set_point_text(self, text: str):
|
|
204
|
+
self.ps.stx.set_text(text)
|
|
205
|
+
|
|
206
|
+
def sharexy(self):
|
|
207
|
+
return self.selections.linkx.isChecked(
|
|
208
|
+
), self.selections.linky.isChecked()
|
|
209
|
+
|
|
210
|
+
def set_mainwindow(self, mainwindow):
|
|
211
|
+
self.mainwindow = mainwindow
|
|
212
|
+
self.AR.clicked.connect(self.mainwindow.all_enable_auto_range)
|
|
213
|
+
self.CR.clicked.connect(self.CR_action)
|
|
214
|
+
|
|
215
|
+
def AR_action(self):
|
|
216
|
+
self.mainwindow.enable_all_auto_range()
|
|
217
|
+
|
|
218
|
+
def CR_action(self):
|
|
219
|
+
self.CR_flag = True
|
|
220
|
+
|
|
221
|
+
def refresh_layout(self):
|
|
222
|
+
if self.mode == 'P':
|
|
223
|
+
self.ts.rm4l()
|
|
224
|
+
self.ps.a2l(self.layout)
|
|
225
|
+
elif self.mode == 'T':
|
|
226
|
+
self.ps.rm4l()
|
|
227
|
+
self.ts.a2l(self.layout)
|
|
228
|
+
|
|
229
|
+
def toggle_mode(self):
|
|
230
|
+
if (self.pb.isChecked()):
|
|
231
|
+
self.mode = 'P'
|
|
232
|
+
elif (self.tb.isChecked()):
|
|
233
|
+
self.mode = 'T'
|
|
234
|
+
self.refresh_layout()
|
|
235
|
+
self.refresh_comb()
|
|
236
|
+
|
|
237
|
+
def refresh_comb(self, ):
|
|
238
|
+
self.generateXYFM()
|
|
239
|
+
self.textEdited()
|
|
240
|
+
#set tooltips
|
|
241
|
+
self.ytxt_lb.setToolTip(str(self.column_names))
|
|
242
|
+
|
|
243
|
+
def link_edited(self):
|
|
244
|
+
# print("LinkEdited")
|
|
245
|
+
self.link_dirty = True
|
|
246
|
+
|
|
247
|
+
def generateXYFM(self):
|
|
248
|
+
self.fx = self.selections.fx.currentText()
|
|
249
|
+
self.fy = self.selections.fy.currentText()
|
|
250
|
+
self.lx = self.selections.lx.isChecked()
|
|
251
|
+
self.ly = self.selections.ly.isChecked()
|
|
252
|
+
self.xyfm_dirty = True
|
|
253
|
+
#self.show_info() ;
|
|
254
|
+
|
|
255
|
+
def textEdited(self):
|
|
256
|
+
new_pairs = matched_xy_pairs(self.selections.stx.text(),
|
|
257
|
+
self.column_names)
|
|
258
|
+
if (len(self.xypairs) != len(new_pairs)):
|
|
259
|
+
self.xypairs_dirty = True
|
|
260
|
+
else:
|
|
261
|
+
for i, xy in enumerate(new_pairs):
|
|
262
|
+
if (xy != self.xypairs[i]):
|
|
263
|
+
self.xypairs_dirty = True
|
|
264
|
+
break
|
|
265
|
+
self.xypairs = new_pairs
|
qulab/scan/__init__.py
ADDED
qulab/scan/curd.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from datetime import date, datetime, timezone
|
|
2
|
+
from typing import Sequence, Type, Union
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.orm import Query, Session, aliased
|
|
5
|
+
from sqlalchemy.orm.exc import NoResultFound
|
|
6
|
+
from sqlalchemy.orm.session import Session
|
|
7
|
+
from waveforms.dicttree import foldDict
|
|
8
|
+
|
|
9
|
+
from .models import Comment, Record, Report, Sample, Tag
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tag(session: Session, tag_text: str) -> Tag:
|
|
13
|
+
"""Get a tag from the database or create a new if not exists."""
|
|
14
|
+
try:
|
|
15
|
+
return session.query(Tag).filter(Tag.text == tag_text).one()
|
|
16
|
+
except NoResultFound:
|
|
17
|
+
tag = Tag(text=tag_text)
|
|
18
|
+
return tag
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tag_it(session: Session, tag_text: str, obj: Union[Sample, Record,
|
|
22
|
+
Report]) -> Tag:
|
|
23
|
+
"""Tag an object."""
|
|
24
|
+
if obj.id is None:
|
|
25
|
+
session.add(obj)
|
|
26
|
+
obj.tags.append(tag(session, tag_text))
|
|
27
|
+
else:
|
|
28
|
+
session.query(type(obj)).filter(
|
|
29
|
+
type(obj).id == obj.id).one().tags.append(tag(session, tag_text))
|
|
30
|
+
session.commit()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_object_with_tags(session: Session,
|
|
34
|
+
cls: Union[Type[Comment], Type[Sample], Type[Record],
|
|
35
|
+
Type[Report]], *tags: str) -> Query:
|
|
36
|
+
"""
|
|
37
|
+
Query objects with the given tags.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
session : :class:`sqlalchemy.orm.Session`
|
|
42
|
+
The database session.
|
|
43
|
+
cls : :class:`sqlalchemy.orm.Mapper`
|
|
44
|
+
The object class.
|
|
45
|
+
tags : str
|
|
46
|
+
The tags.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
:class:`sqlalchemy.orm.Query`
|
|
51
|
+
The query.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(session, Query):
|
|
54
|
+
q = session
|
|
55
|
+
else:
|
|
56
|
+
q = session.query(cls)
|
|
57
|
+
if not hasattr(cls, 'tags'):
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
aliase = {tag: aliased(Tag) for tag in tags}
|
|
61
|
+
|
|
62
|
+
for tag, a in aliase.items():
|
|
63
|
+
q = q.join(a, cls.tags)
|
|
64
|
+
if '*' in tag:
|
|
65
|
+
q = q.filter(a.text.like(tag.replace('*', '%')))
|
|
66
|
+
else:
|
|
67
|
+
q = q.filter(a.text == tag)
|
|
68
|
+
return q
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def query_record(session: Session,
|
|
72
|
+
offset: int = 0,
|
|
73
|
+
limit: int = 10,
|
|
74
|
+
app: str | None = None,
|
|
75
|
+
tags: Sequence[str] = (),
|
|
76
|
+
before: datetime | date | None = None,
|
|
77
|
+
after: datetime | date | None = None):
|
|
78
|
+
tz_offset = datetime.now(timezone.utc).astimezone().utcoffset()
|
|
79
|
+
table = {'header': ['ID', 'App', 'tags', 'created time'], 'body': []}
|
|
80
|
+
apps = sorted(
|
|
81
|
+
set([
|
|
82
|
+
n for n, *_ in get_object_with_tags(session.query(Record.app),
|
|
83
|
+
Record, *tags).all()
|
|
84
|
+
]))
|
|
85
|
+
apps = foldDict(dict([(app, None) for app in apps]))
|
|
86
|
+
|
|
87
|
+
query = get_object_with_tags(session, Record, *tags)
|
|
88
|
+
|
|
89
|
+
if app is not None:
|
|
90
|
+
if app.endswith('*'):
|
|
91
|
+
query = query.filter(Record.app.like(app[:-1] + '%'))
|
|
92
|
+
else:
|
|
93
|
+
query = query.filter(Record.app == app)
|
|
94
|
+
if before is not None:
|
|
95
|
+
if isinstance(before, date):
|
|
96
|
+
before = datetime(before.year, before.month, before.day)
|
|
97
|
+
query = query.filter(Record.ctime <= before - tz_offset)
|
|
98
|
+
if after is not None:
|
|
99
|
+
if isinstance(after, date):
|
|
100
|
+
after = datetime(after.year, after.month, after.day)
|
|
101
|
+
query = query.filter(Record.ctime >= after - tz_offset)
|
|
102
|
+
total = query.count()
|
|
103
|
+
for r in query.order_by(Record.ctime.desc()).limit(limit).offset(offset):
|
|
104
|
+
tags = sorted([t.text for t in r.tags])
|
|
105
|
+
ctime = r.ctime + tz_offset
|
|
106
|
+
row = [r.id, r.app, tags, ctime]
|
|
107
|
+
table['body'].append(row)
|
|
108
|
+
|
|
109
|
+
return total, apps, table
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def update_tags(session: Session,
|
|
113
|
+
record_id: int,
|
|
114
|
+
tags: Sequence[str],
|
|
115
|
+
append: bool = False):
|
|
116
|
+
record = session.get(Record, record_id)
|
|
117
|
+
if record is None:
|
|
118
|
+
return False
|
|
119
|
+
if append:
|
|
120
|
+
old = [t.text for t in record.tags]
|
|
121
|
+
for t in old:
|
|
122
|
+
if t not in tags:
|
|
123
|
+
tags.append(t)
|
|
124
|
+
record.tags = [tag(session, t) for t in tags]
|
|
125
|
+
try:
|
|
126
|
+
session.commit()
|
|
127
|
+
except Exception:
|
|
128
|
+
session.rollback()
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def remove_tags(session: Session, record_id: int, tags: Sequence[str]):
|
|
134
|
+
record = session.get(Record, record_id)
|
|
135
|
+
if record is None:
|
|
136
|
+
return False
|
|
137
|
+
old = [t.text for t in record.tags]
|
|
138
|
+
record.tags = [tag(session, t) for t in old if t not in tags]
|
|
139
|
+
try:
|
|
140
|
+
session.commit()
|
|
141
|
+
except Exception:
|
|
142
|
+
session.rollback()
|
|
143
|
+
return False
|
|
144
|
+
return True
|