311 lines
12 KiB
Python
311 lines
12 KiB
Python
import collections, math
|
|
|
|
from Qt import QtCore
|
|
import pyqtgraph as pg
|
|
|
|
# Basic PyQtGraph settings
|
|
pg.setConfigOptions(antialias=True)
|
|
|
|
|
|
class SpectrumPlotWidget:
|
|
"""Main spectrum plot"""
|
|
def __init__(self, layout):
|
|
if not isinstance(layout, pg.GraphicsLayoutWidget):
|
|
raise ValueError("layout must be instance of pyqtgraph.GraphicsLayoutWidget")
|
|
|
|
self.layout = layout
|
|
|
|
self.main_curve = True
|
|
self.main_color = pg.mkColor("y")
|
|
self.persistence = False
|
|
self.persistence_length = 5
|
|
self.persistence_decay = "exponential"
|
|
self.persistence_color = pg.mkColor("g")
|
|
self.persistence_data = None
|
|
self.persistence_curves = None
|
|
self.peak_hold_max = False
|
|
self.peak_hold_max_color = pg.mkColor("r")
|
|
self.peak_hold_min = False
|
|
self.peak_hold_min_color = pg.mkColor("b")
|
|
self.average = False
|
|
self.average_color = pg.mkColor("c")
|
|
|
|
self.create_plot()
|
|
|
|
def create_plot(self):
|
|
"""Create main spectrum plot"""
|
|
self.posLabel = self.layout.addLabel(row=0, col=0, justify="right")
|
|
self.plot = self.layout.addPlot(row=1, col=0)
|
|
self.plot.showGrid(x=True, y=True)
|
|
self.plot.setLabel("left", "Power", units="dB")
|
|
self.plot.setLabel("bottom", "Frequency", units="Hz")
|
|
self.plot.setLimits(xMin=0)
|
|
self.plot.showButtons()
|
|
|
|
#self.plot.setDownsampling(mode="peak")
|
|
#self.plot.setClipToView(True)
|
|
|
|
self.create_persistence_curves()
|
|
self.create_average_curve()
|
|
self.create_peak_hold_min_curve()
|
|
self.create_peak_hold_max_curve()
|
|
self.create_main_curve()
|
|
|
|
# Create crosshair
|
|
self.vLine = pg.InfiniteLine(angle=90, movable=False)
|
|
self.vLine.setZValue(1000)
|
|
self.hLine = pg.InfiniteLine(angle=0, movable=False)
|
|
self.vLine.setZValue(1000)
|
|
self.plot.addItem(self.vLine, ignoreBounds=True)
|
|
self.plot.addItem(self.hLine, ignoreBounds=True)
|
|
self.mouseProxy = pg.SignalProxy(self.plot.scene().sigMouseMoved,
|
|
rateLimit=60, slot=self.mouse_moved)
|
|
|
|
def create_main_curve(self):
|
|
"""Create main spectrum curve"""
|
|
self.curve = self.plot.plot(pen=self.main_color)
|
|
self.curve.setZValue(900)
|
|
|
|
def create_peak_hold_max_curve(self):
|
|
"""Create max. peak hold curve"""
|
|
self.curve_peak_hold_max = self.plot.plot(pen=self.peak_hold_max_color)
|
|
self.curve_peak_hold_max.setZValue(800)
|
|
|
|
def create_peak_hold_min_curve(self):
|
|
"""Create min. peak hold curve"""
|
|
self.curve_peak_hold_min = self.plot.plot(pen=self.peak_hold_min_color)
|
|
self.curve_peak_hold_min.setZValue(800)
|
|
|
|
def create_average_curve(self):
|
|
"""Create average curve"""
|
|
self.curve_average = self.plot.plot(pen=self.average_color)
|
|
self.curve_average.setZValue(700)
|
|
|
|
def create_persistence_curves(self):
|
|
"""Create spectrum persistence curves"""
|
|
z_index_base = 600
|
|
decay = self.get_decay()
|
|
self.persistence_curves = []
|
|
for i in range(self.persistence_length):
|
|
alpha = 255 * decay(i + 1, self.persistence_length + 1)
|
|
color = self.persistence_color
|
|
curve = self.plot.plot(pen=(color.red(), color.green(), color.blue(), alpha))
|
|
curve.setZValue(z_index_base - i)
|
|
self.persistence_curves.append(curve)
|
|
|
|
def set_colors(self):
|
|
"""Set colors of all curves"""
|
|
self.curve.setPen(self.main_color)
|
|
self.curve_peak_hold_max.setPen(self.peak_hold_max_color)
|
|
self.curve_peak_hold_min.setPen(self.peak_hold_min_color)
|
|
self.curve_average.setPen(self.average_color)
|
|
|
|
decay = self.get_decay()
|
|
for i, curve in enumerate(self.persistence_curves):
|
|
alpha = 255 * decay(i + 1, self.persistence_length + 1)
|
|
color = self.persistence_color
|
|
curve.setPen((color.red(), color.green(), color.blue(), alpha))
|
|
|
|
def decay_linear(self, x, length):
|
|
"""Get alpha value for persistence curve (linear decay)"""
|
|
return (-x / length) + 1
|
|
|
|
def decay_exponential(self, x, length, const=1 / 3):
|
|
"""Get alpha value for persistence curve (exponential decay)"""
|
|
return math.e**(-x / (length * const))
|
|
|
|
def get_decay(self):
|
|
"""Get decay function"""
|
|
if self.persistence_decay == 'exponential':
|
|
return self.decay_exponential
|
|
else:
|
|
return self.decay_linear
|
|
|
|
def update_plot(self, data_storage, force=False):
|
|
"""Update main spectrum curve"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
if self.main_curve or force:
|
|
self.curve.setData(data_storage.x, data_storage.y)
|
|
if force:
|
|
self.curve.setVisible(self.main_curve)
|
|
|
|
def update_peak_hold_max(self, data_storage, force=False):
|
|
"""Update max. peak hold curve"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
if self.peak_hold_max or force:
|
|
self.curve_peak_hold_max.setData(data_storage.x, data_storage.peak_hold_max)
|
|
if force:
|
|
self.curve_peak_hold_max.setVisible(self.peak_hold_max)
|
|
|
|
def update_peak_hold_min(self, data_storage, force=False):
|
|
"""Update min. peak hold curve"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
if self.peak_hold_min or force:
|
|
self.curve_peak_hold_min.setData(data_storage.x, data_storage.peak_hold_min)
|
|
if force:
|
|
self.curve_peak_hold_min.setVisible(self.peak_hold_min)
|
|
|
|
def update_average(self, data_storage, force=False):
|
|
"""Update average curve"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
if self.average or force:
|
|
self.curve_average.setData(data_storage.x, data_storage.average)
|
|
if force:
|
|
self.curve_average.setVisible(self.average)
|
|
|
|
def update_persistence(self, data_storage, force=False):
|
|
"""Update persistence curves"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
if self.persistence or force:
|
|
if self.persistence_data is None:
|
|
self.persistence_data = collections.deque(maxlen=self.persistence_length)
|
|
else:
|
|
for i, y in enumerate(self.persistence_data):
|
|
curve = self.persistence_curves[i]
|
|
curve.setData(data_storage.x, y)
|
|
if force:
|
|
curve.setVisible(self.persistence)
|
|
self.persistence_data.appendleft(data_storage.y)
|
|
|
|
def recalculate_plot(self, data_storage):
|
|
"""Recalculate plot from history"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
QtCore.QTimer.singleShot(0, lambda: self.update_plot(data_storage, force=True))
|
|
QtCore.QTimer.singleShot(0, lambda: self.update_average(data_storage, force=True))
|
|
QtCore.QTimer.singleShot(0, lambda: self.update_peak_hold_max(data_storage, force=True))
|
|
QtCore.QTimer.singleShot(0, lambda: self.update_peak_hold_min(data_storage, force=True))
|
|
|
|
def recalculate_persistence(self, data_storage):
|
|
"""Recalculate persistence data and update persistence curves"""
|
|
if data_storage.x is None:
|
|
return
|
|
|
|
self.clear_persistence()
|
|
self.persistence_data = collections.deque(maxlen=self.persistence_length)
|
|
for i in range(min(self.persistence_length, data_storage.history.history_size - 1)):
|
|
data = data_storage.history[-i - 2]
|
|
if data_storage.smooth:
|
|
data = data_storage.smooth_data(data)
|
|
self.persistence_data.append(data)
|
|
QtCore.QTimer.singleShot(0, lambda: self.update_persistence(data_storage, force=True))
|
|
|
|
def mouse_moved(self, evt):
|
|
"""Update crosshair when mouse is moved"""
|
|
pos = evt[0]
|
|
if self.plot.sceneBoundingRect().contains(pos):
|
|
mousePoint = self.plot.vb.mapSceneToView(pos)
|
|
self.posLabel.setText(
|
|
"<span style='font-size: 12pt'>f={:0.3f} MHz, P={:0.3f} dB</span>".format(
|
|
mousePoint.x() / 1e6,
|
|
mousePoint.y()
|
|
)
|
|
)
|
|
self.vLine.setPos(mousePoint.x())
|
|
self.hLine.setPos(mousePoint.y())
|
|
|
|
def clear_plot(self):
|
|
"""Clear main spectrum curve"""
|
|
self.curve.clear()
|
|
|
|
def clear_peak_hold_max(self):
|
|
"""Clear max. peak hold curve"""
|
|
self.curve_peak_hold_max.clear()
|
|
|
|
def clear_peak_hold_min(self):
|
|
"""Clear min. peak hold curve"""
|
|
self.curve_peak_hold_min.clear()
|
|
|
|
def clear_average(self):
|
|
"""Clear average curve"""
|
|
self.curve_average.clear()
|
|
|
|
def clear_persistence(self):
|
|
"""Clear spectrum persistence curves"""
|
|
self.persistence_data = None
|
|
for curve in self.persistence_curves:
|
|
curve.clear()
|
|
self.plot.removeItem(curve)
|
|
self.create_persistence_curves()
|
|
|
|
|
|
class WaterfallPlotWidget:
|
|
"""Waterfall plot"""
|
|
def __init__(self, layout, histogram_layout=None):
|
|
if not isinstance(layout, pg.GraphicsLayoutWidget):
|
|
raise ValueError("layout must be instance of pyqtgraph.GraphicsLayoutWidget")
|
|
|
|
if histogram_layout and not isinstance(histogram_layout, pg.GraphicsLayoutWidget):
|
|
raise ValueError("histogram_layout must be instance of pyqtgraph.GraphicsLayoutWidget")
|
|
|
|
self.layout = layout
|
|
self.histogram_layout = histogram_layout
|
|
|
|
self.history_size = 100
|
|
self.counter = 0
|
|
|
|
self.create_plot()
|
|
|
|
def create_plot(self):
|
|
"""Create waterfall plot"""
|
|
self.plot = self.layout.addPlot()
|
|
self.plot.setLabel("bottom", "Frequency", units="Hz")
|
|
self.plot.setLabel("left", "Time")
|
|
|
|
self.plot.setYRange(-self.history_size, 0)
|
|
self.plot.setLimits(xMin=0, yMax=0)
|
|
self.plot.showButtons()
|
|
#self.plot.setAspectLocked(True)
|
|
|
|
#self.plot.setDownsampling(mode="peak")
|
|
#self.plot.setClipToView(True)
|
|
|
|
# Setup histogram widget (for controlling waterfall plot levels and gradients)
|
|
if self.histogram_layout:
|
|
self.histogram = pg.HistogramLUTItem()
|
|
self.histogram_layout.addItem(self.histogram)
|
|
self.histogram.gradient.loadPreset("flame")
|
|
#self.histogram.setHistogramRange(-50, 0)
|
|
#self.histogram.setLevels(-50, 0)
|
|
|
|
def update_plot(self, data_storage):
|
|
"""Update waterfall plot"""
|
|
self.counter += 1
|
|
|
|
# Create waterfall image on first run
|
|
if self.counter == 1:
|
|
self.waterfallImg = pg.ImageItem()
|
|
self.waterfallImg.scale((data_storage.x[-1] - data_storage.x[0]) / len(data_storage.x), 1)
|
|
self.plot.clear()
|
|
self.plot.addItem(self.waterfallImg)
|
|
|
|
# Roll down one and replace leading edge with new data
|
|
self.waterfallImg.setImage(data_storage.history.buffer[-self.counter:].T,
|
|
autoLevels=False, autoRange=False)
|
|
|
|
# Move waterfall image to always start at 0
|
|
self.waterfallImg.setPos(
|
|
data_storage.x[0],
|
|
-self.counter if self.counter < self.history_size else -self.history_size
|
|
)
|
|
|
|
# Link histogram widget to waterfall image on first run
|
|
# (must be done after first data is received or else levels would be wrong)
|
|
if self.counter == 1 and self.histogram_layout:
|
|
self.histogram.setImageItem(self.waterfallImg)
|
|
|
|
def clear_plot(self):
|
|
"""Clear waterfall plot"""
|
|
self.counter = 0
|