-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmonitor.py
executable file
·306 lines (295 loc) · 16.5 KB
/
monitor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env python3
# −*− coding:utf-8 −*−
import sys, os
import numpy as np
import pyqtgraph as pg
import matplotlib as mpl
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.uic import *
from datetime import datetime
from processing import Processing
from multithread import Worker
class Data_Monitor(QMainWindow):
'''
A GUI for monitoring the latest data file under a designated directory, then inspecting it in both time and frequency domains
'''
fgcolor = "#23373B"
bgcolor = "#FAFAFA"
blue = "#113285"
green = "#1B813E"
orange = "#E98B2A"
red = "#AB3B3A"
data_file = '' # empty initial file
lut = (mpl.cm.get_cmap("viridis")(np.linspace(0, 1, 256))[:,:3] * 255).astype(np.dtype("u1"))
pg.setConfigOptions(background=bgcolor, foreground=fgcolor, antialias=True, imageAxisOrder="row-major")
def font_label(self, string): return "<span style=font-family:RobotoCondensed;font-size:14pt>" + string + "</span>"
def __init__(self, directory="/home/schospec/Data/"):
'''
paint the user interface and establish the signal-socket connections
directory: location to be sought for data files
'''
super().__init__()
loadUi("monitor.ui", self) # the .ui file is generated by the Qt creator
self.thread_pool = QThreadPool()
self.directory = directory
self.logarithm = self.rLog.isChecked()
self.manual = self.rManual.isChecked()
self.draw_plots()
self.build_connections()
def draw_plots(self):
'''
draw the time signals separately in in-phase and quadrature parts, also the frequency signals separately in 1d and 2d forms
'''
# time plots --- in-phase
self.plot_i = self.gTimeSeries.addPlot(0, 0)
self.plot_i.plot([0, 1], [0, 0], pen=self.blue)
self.plot_i.setLabels(left=self.font_label("In-Phase"), bottom=self.font_label("Time [s]"))
self.plot_i.setRange(xRange=(0, 1), yRange=(-1, 1))
# time plots --- quadrature
self.plot_q = self.gTimeSeries.addPlot(1, 0)
self.plot_q.plot([0, 1], [0, 0], pen=self.blue)
self.plot_q.setLabels(left=self.font_label("Quadrature"), bottom=self.font_label("Time [s]"))
self.plot_q.setRange(xRange=(0, 1), yRange=(-1, 1))
# dummy data for frequency plots
self.frequencies = np.array([-1, 0, 1])
self.times_f = np.array([0, 1])
self.spectrogram = np.array([[1, 1]])
self.frame = 0
self.fill_level = np.floor(np.min(self.spectrogram[self.frame])) if self.logarithm else 0
# frequency plots --- spectrum
self.gSpectrum.plot((self.frequencies[:-1]+self.frequencies[1:])/2, self.spectrogram[self.frame],
pen=self.orange, fillLevel=self.fill_level, fillBrush=self.orange+"80")
self.gSpectrum.setLabels(title=self.font_label("Frame # 0"),
left=self.font_label("Power Spectral Density"), bottom=self.font_label("Frequency − ___ MHz [kHz]"))
self.gSpectrum.setRange(xRange=(self.frequencies[0], self.frequencies[-1]), yRange=(self.times_f[0], self.times_f[-1]))
# frequency plots --- spectrogram
self.img = pg.ImageItem(self.spectrogram)
self.img.setRect(QRectF(-(self.frequencies[-1]-self.frequencies[0])/2, self.times_f[0],
self.frequencies[-1]-self.frequencies[0], self.times_f[-1]-self.times_f[0]))
self.img.setLookupTable(self.lut)
self.gSpectrogram.addItem(self.img)
self.gSpectrogram.setLabels(left=self.font_label("Time [s]"), bottom=self.font_label("Frequency − ___ MHz [kHz]"))
self.gSpectrogram.setRange(xRange=(self.frequencies[0], self.frequencies[-1]), yRange=(self.times_f[0], self.times_f[-1]))
# file list
self.mFileList = QFileSystemModel()
self.mFileList.setFilter(QDir.Files)
self.mFileList.setNameFilters(["*.wvd"])
self.mFileList.setNameFilterDisables(False)
self.mFileList.sort(3, Qt.DescendingOrder) # sort by the fourth column, i.e. modified time
self.vFileList.setModel(self.mFileList)
self.vFileList.setRootIndex(self.mFileList.setRootPath(self.directory))
# acquisition parameters
self.wParaTable.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
self.wParaTable.verticalHeader().setResizeMode(QHeaderView.Stretch)
def build_connections(self):
'''
build the signal-slot connections
'''
# bind time plots
def update_range_i():
self.plot_i.setRange(self.plot_q.getViewBox().viewRect(), padding=0)
def update_range_q():
self.plot_q.setRange(self.plot_i.getViewBox().viewRect(), padding=0)
self.plot_i.sigRangeChanged.connect(update_range_q)
self.plot_q.sigRangeChanged.connect(update_range_i)
# bind frequency plots
self.indicator = self.gSpectrogram.addLine(y=self.times_f[0], bounds=[self.times_f[0], self.times_f[-1]],
pen=self.bgcolor, hoverPen=self.red, movable=True)
def on_dragged(line):
pos = line.value()
self.frame = int((pos - self.times_f[0]) / (self.times_f[1] - self.times_f[0]))
self.frame -= 1 if self.frame == self.times_f.size-1 else 0
self.fill_level = np.floor(np.min(self.spectrogram[self.frame])) if self.logarithm else 0
self.gSpectrum.listDataItems()[0].setData((self.frequencies[:-1]+self.frequencies[1:])/2, self.spectrogram[self.frame])
self.gSpectrum.listDataItems()[0].setFillLevel(self.fill_level)
current_range = self.gSpectrum.getViewBox().viewRange()[0]
self.gSpectrum.autoRange()
self.gSpectrum.setXRange(*current_range, padding=0)
self.gSpectrum.setTitle(self.font_label("Frame # {:d}".format(self.frame)))
self.indicator.sigPositionChanged.connect(on_dragged)
def update_range_spectrum():
self.gSpectrum.setXRange(*self.gSpectrogram.getViewBox().viewRange()[0], padding=0)
def update_range_spectrogram():
self.gSpectrogram.setXRange(*self.gSpectrum.getViewBox().viewRange()[0], padding=0)
self.gSpectrum.sigRangeChanged.connect(update_range_spectrogram)
self.gSpectrogram.sigRangeChanged.connect(update_range_spectrum)
# read coordinates at the cursor in time plots
def on_moved_i(point):
if self.plot_i.sceneBoundingRect().contains(point):
coords = self.plot_i.getViewBox().mapSceneToView(point)
self.statusbar.showMessage("t = {:.5g} s, i = {:.5g}".format(coords.x(), coords.y()))
def on_moved_q(point):
if self.plot_q.sceneBoundingRect().contains(point):
coords = self.plot_q.getViewBox().mapSceneToView(point)
self.statusbar.showMessage("t = {:.5g} s, q = {:.5g}".format(coords.x(), coords.y()))
self.plot_i.scene().sigMouseMoved.connect(on_moved_i)
self.plot_q.scene().sigMouseMoved.connect(on_moved_q)
# read coordinates at the cursor in frequency plots
self.crosshair_h = pg.InfiniteLine(pos=self.fill_level, angle=0, pen=self.fgcolor)
self.crosshair_v = pg.InfiniteLine(pos=0, angle=90, pen=self.fgcolor)
self.gSpectrum.addItem(self.crosshair_h, ignoreBounds=True)
self.gSpectrum.addItem(self.crosshair_v, ignoreBounds=True)
def on_moved_spectrum(point):
if self.gSpectrum.sceneBoundingRect().contains(point):
coords = self.gSpectrum.getViewBox().mapSceneToView(point)
self.crosshair_h.setValue(coords.y())
self.crosshair_v.setValue(coords.x())
self.statusbar.showMessage("δf = {:.5g} kHz, t = {:.5g} s, psd = {:.5g}".format(coords.x(), self.indicator.value(), coords.y()))
self.gSpectrum.scene().sigMouseMoved.connect(on_moved_spectrum)
def on_moved_spectrogram(point):
if self.gSpectrogram.sceneBoundingRect().contains(point):
coords = self.gSpectrogram.getViewBox().mapSceneToView(point)
self.crosshair_v.setValue(coords.x())
self.statusbar.showMessage("δf = {:.5g} kHz, t = {:.5g} s, psd = {:.5g}"
.format(coords.x(), self.indicator.value(), self.crosshair_h.value()))
self.gSpectrogram.scene().sigMouseMoved.connect(on_moved_spectrogram)
def on_clicked(event):
point = event.scenePos()
if self.gSpectrogram.sceneBoundingRect().contains(point):
coords = self.gSpectrogram.getViewBox().mapSceneToView(point)
self.indicator.setValue(coords.y())
self.statusbar.showMessage("δf = {:.5g} kHz, t = {:.5g} s, psd = {:.5g}"
.format(self.crosshair_v.value(), coords.y(), self.crosshair_h.value()))
self.gSpectrogram.scene().sigMouseClicked.connect(on_clicked)
# toggle the linear or logarithmic scale, auto or manual refresh mode
def on_toggled_scale():
self.logarithm = self.rLog.isChecked()
if self.logarithm:
self.spectrogram = np.log10(self.spectrogram)
else:
self.spectrogram = np.power(10, self.spectrogram)
self.img.setImage(self.spectrogram)
self.fill_level = np.floor(np.min(self.spectrogram[self.frame])) if self.logarithm else 0
self.gSpectrum.listDataItems()[0].setData((self.frequencies[:-1]+self.frequencies[1:])/2, self.spectrogram[self.frame])
self.gSpectrum.listDataItems()[0].setFillLevel(self.fill_level)
current_range = self.gSpectrum.getViewBox().viewRange()[0]
self.gSpectrum.autoRange()
self.gSpectrum.setXRange(*current_range, padding=0)
self.rLin.toggled.connect(on_toggled_scale)
def on_toggled_refresh():
self.manual = self.rManual.isChecked()
if self.manual:
self.vFileList.activated.connect(selected_file)
self.mFileList.rowsInserted.disconnect(last_file)
self.mFileList.rowsRemoved.disconnect(last_file)
else:
self.vFileList.clearSelection()
self.vFileList.activated.disconnect(selected_file)
self.mFileList.rowsInserted.connect(last_file)
self.mFileList.rowsRemoved.connect(last_file)
self.rAuto.toggled.connect(on_toggled_refresh)
# select a file for analysis
def selected_file(model_index):
self.prepare_data(model_index.data())
def last_file(model_index):
QTimer.singleShot(700, lambda: self.prepare_data(model_index.child(0,0).data())) # wait for file transfer completion
if self.manual:
self.vFileList.activated.connect(selected_file)
else:
self.mFileList.rowsInserted.connect(last_file)
self.mFileList.rowsRemoved.connect(last_file)
# Ctrl+W or Ctrl+Q to quit the application
shortcutW = QShortcut(QKeySequence.Close, self)
shortcutQ = QShortcut(QKeySequence.Quit, self)
shortcutW.activated.connect(self.close)
shortcutQ.activated.connect(self.close)
# Ctrl+S to take a screenshot
def screenShot():
timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
fname = '_'.join(["screenshot", self.file_name[:-4], timestamp])
if not os.path.isdir("./screenshots"):
os.mkdir("./screenshots")
self.statusbar.showMessage("`./screenshots` created")
self.grab(QRect(0,0,-1,-1)).save("./screenshots/" + fname + ".png")
self.statusbar.showMessage('`' + fname + ".png` saved")
shortcutS = QShortcut(QKeySequence.Save, self)
shortcutS.activated.connect(screenShot)
def prepare_data(self, data_file):
'''
load the data in time domain from disk, and compute the frequency data
'''
if self.data_file == data_file: # duplicate work is inadvisable
return
else:
self.data_file = data_file
processing = Processing(self.directory+self.data_file)
# extract acquisition parameters
self.file_name = processing.fname
self.timestamp = str(processing.date_time)
self.ref_level = processing.ref_level # dBm
self.sampling_rate = processing.sampling_rate / 1e3 # kHz
self.n_sample = processing.n_sample # IQ pairs
self.span = processing.span
self.center_frequency = processing.center_frequency
# data in time domain, on a spin-off thread
worker_t = Worker(super(Processing, processing).diagnosis, n_point=processing.n_buffer, draw=False)
worker_t.signals.result.connect(self.redraw_time_plots)
self.thread_pool.start(worker_t)
# data in frequency domain, on another thread
worker_f = Worker(processing.time_average_2d, window_length=2000, n_frame=-1,
padding_ratio=1, n_offset=0, n_average=10, estimator='p', window="kaiser", beta=14)
worker_f.signals.result.connect(self.redraw_frequency_plots)
self.thread_pool.start(worker_f)
# status bar changes only if two threads both terminate
self.statusbar.showMessage("updating...")
self.working_threads = 2
def wait_for_finish():
if self.working_threads == 0:
return self.statusbar.clearMessage()
else:
QTimer.singleShot(100, wait_for_finish)
wait_for_finish()
def redraw_time_plots(self, args):
'''
redraw the in-phase and quadrature plots, and update the parameter table
'''
self.times_t, self.iqs = args # s, V
# acquisition parameters
self.wParaTable.setItem(0, 1, QTableWidgetItem(self.file_name))
self.wParaTable.setItem(1, 1, QTableWidgetItem(self.timestamp))
self.wParaTable.setItem(2, 1, QTableWidgetItem("{:g} dBm".format(self.ref_level)))
self.wParaTable.setItem(3, 1, QTableWidgetItem("{:g} kHz".format(self.sampling_rate)))
self.wParaTable.setItem(4, 1, QTableWidgetItem("{:,d}".format(self.n_sample)))
# time plots --- in-phase
self.plot_i.listDataItems()[0].setData(self.times_t, np.real(self.iqs))
self.plot_i.setRange(xRange=(self.times_t[0], self.times_t[-1]), yRange=(-1, 1))
# time plots --- quadrature
self.plot_q.listDataItems()[0].setData(self.times_t, np.imag(self.iqs))
self.plot_q.setRange(xRange=(self.times_t[0], self.times_t[-1]), yRange=(-1, 1))
self.working_threads -= 1
def redraw_frequency_plots(self, args):
'''
redraw the frequency spectrum and spectrogram
'''
frequencies, self.times_f, spectrogram = args[:3] # kHz, s, V^2/kHz
index_l = np.argmin(np.abs(frequencies+self.span/2e3))
index_r = np.argmin(np.abs(frequencies-self.span/2e3)) + 1
self.frequencies = frequencies[index_l:index_r] # kHz
spectrogram = spectrogram[:,index_l:index_r-1] # V^2/kHz
self.spectrogram = np.log10(spectrogram) if self.logarithm else spectrogram
self.frame = 0
self.fill_level = np.floor(np.min(self.spectrogram[self.frame])) if self.logarithm else 0
# frequency plots --- spectrum
self.gSpectrum.listDataItems()[0].setData((self.frequencies[:-1]+self.frequencies[1:])/2, self.spectrogram[self.frame])
self.gSpectrum.listDataItems()[0].setFillLevel(self.fill_level)
self.gSpectrum.setLabels(title=self.font_label("Frame # 0"), bottom=self.font_label("Frequency − {:g} MHz [kHz]".format(self.center_frequency/1e6)))
self.gSpectrum.setRange(xRange=(-self.span/2e3, self.span/2e3), yRange=(self.fill_level, np.max(self.spectrogram[self.frame])))
# frequency plots --- spectrogram
self.img.setImage(self.spectrogram)
self.img.setRect(QRectF(-(self.frequencies[-1]-self.frequencies[0])/2, self.times_f[0],
self.frequencies[-1]-self.frequencies[0], self.times_f[-1]-self.times_f[0]))
self.gSpectrogram.setLabels(bottom=self.font_label("Frequency − {:g} MHz [kHz]".format(self.center_frequency/1e6)))
self.gSpectrogram.setRange(xRange=(-self.span/2e3, self.span/2e3), yRange=(self.times_f[0], self.times_f[-1]))
# reset markers
self.crosshair_h.setValue(self.fill_level)
self.crosshair_v.setValue(0)
self.indicator.setValue(self.times_f[0])
self.indicator.setBounds([self.times_f[0], self.times_f[-1]])
self.working_threads -= 1
if __name__ == "__main__":
app = QApplication(sys.argv)
data_monitor = Data_Monitor()
data_monitor.show()
sys.exit(app.exec())