import logging
from functools import partial
import pyctbgui.utils.pixelmap as pm
import random
from pathlib import Path
import numpy as np
from PyQt5 import QtWidgets, QtGui, uic
import pyqtgraph as pg
from pyctbgui.utils import recordOrApplyPedestal
from pyqtgraph import PlotWidget
from pyctbgui.utils.defines import Defines
class PlotTab(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.frame_min: float = 0.0
self.frame_max: float = 0.0
uic.loadUi(Path(__file__).parent.parent / 'ui' / "plot.ui", parent)
self.view = parent
self.mainWindow = None
self.det = None
self.signalsTab = None
self.transceiverTab = None
self.acquisitionTab = None
self.adcTab = None
self.cmin: float = 0.0
self.cmax: float = 0.0
self.colorRangeMode: Defines.colorRange = Defines.colorRange.all
self.ignoreHistogramSignal: bool = False
self.imagePlots: list[PlotWidget] = []
# list of callback functions to notify tabs when we should hide their legend
# follows the observer design pattern
self.hideLegendObservers = []
self.pedestalRecord: bool = False
self.pedestalApply: bool = True
self.__acqFrames = None
self.logger = logging.getLogger('PlotTab')
def setup_ui(self):
self.signalsTab = self.mainWindow.signalsTab
self.transceiverTab = self.mainWindow.transceiverTab
self.acquisitionTab = self.mainWindow.acquisitionTab
self.adcTab = self.mainWindow.adcTab
self.initializeColorMaps()
self.imagePlots = (
self.mainWindow.plotAnalogImage,
self.mainWindow.plotDigitalImage,
self.mainWindow.plotTransceiverImage,
)
def connect_ui(self):
self.view.radioButtonNoPlot.clicked.connect(self.plotOptions)
self.view.radioButtonWaveform.clicked.connect(self.plotOptions)
self.view.radioButtonDistribution.clicked.connect(self.plotOptions)
self.view.radioButtonImage.clicked.connect(self.plotOptions)
self.view.comboBoxPlot.currentIndexChanged.connect(self.setPixelMap)
self.view.comboBoxColorMap.currentIndexChanged.connect(self.setColorMap)
self.view.comboBoxZMQHWM.currentIndexChanged.connect(self.setZMQHWM)
self.view.spinBoxSerialOffset.editingFinished.connect(self.setSerialOffset)
self.view.spinBoxNCount.editingFinished.connect(self.setNCounter)
self.view.spinBoxDynamicRange.editingFinished.connect(self.setDynamicRange)
self.view.spinBoxImageX.editingFinished.connect(self.setImageX)
self.view.spinBoxImageY.editingFinished.connect(self.setImageY)
self.view.radioButtonPedestalRecord.toggled.connect(self.togglePedestalRecord)
self.view.radioButtonPedestalApply.toggled.connect(self.togglePedestalApply)
self.view.pushButtonPedestalReset.clicked.connect(self.resetPedestal)
self.view.pushButtonSavePedestal.clicked.connect(self.savePedestal)
self.view.pushButtonLoadPedestal.clicked.connect(self.loadPedestal)
self.view.checkBoxRaw.stateChanged.connect(self.setRawData)
self.view.spinBoxRawMin.editingFinished.connect(self.setRawData)
self.view.spinBoxRawMax.editingFinished.connect(self.setRawData)
self.view.checkBoxPedestal.stateChanged.connect(self.setPedestalSubtract)
self.view.spinBoxPedestalMin.editingFinished.connect(self.setPedestalSubtract)
self.view.spinBoxPedestalMax.editingFinished.connect(self.setPedestalSubtract)
self.view.spinBoxFit.editingFinished.connect(self.setFitADC)
self.view.spinBoxPlot.editingFinished.connect(self.setPlotBit)
self.view.pushButtonReferesh.clicked.connect(self.acquisitionTab.refresh)
# color range widgets
self.view.cminSpinBox.editingFinished.connect(self.setCmin)
self.view.cmaxSpinBox.editingFinished.connect(self.setCmax)
self.view.radioButtonAutomatic.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.all))
self.view.radioButtonFixed.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.fixed))
self.view.radioButtonCenter.clicked.connect(partial(self.setColorRangeMode, Defines.colorRange.center))
for plot in self.imagePlots:
plot.scene.sigMouseMoved.connect(partial(self.showPlotValues, plot))
plot.getHistogramWidget().item.sigLevelChangeFinished.connect(partial(self.handleHistogramChange, plot))
self.view.checkBoxShowLegend.stateChanged.connect(self.toggleLegend)
def refresh(self):
self.getZMQHWM()
def initializeColorMaps(self):
self.view.comboBoxColorMap.addItems(Defines.Color_map)
self.view.comboBoxColorMap.setCurrentIndex(Defines.Color_map.index(Defines.Default_Color_Map))
self.setColorMap()
def savePedestal(self):
"""
slot function to save pedestal values
"""
response = QtWidgets.QFileDialog.getSaveFileName(self.view, "Save Pedestal", str(self.det.fpath))
recordOrApplyPedestal.savePedestal(Path(response[0]))
self.logger.info(f'saved Pedestal in {response[0]}')
def loadPedestal(self):
"""
slot function to load pedestal values
"""
response = QtWidgets.QFileDialog.getOpenFileName(self.view, "Load Pedestal", str(self.det.fpath))
if response[0] == '':
return
recordOrApplyPedestal.reset(self)
try:
recordOrApplyPedestal.loadPedestal(Path(response[0]))
except (IsADirectoryError, ValueError, EOFError):
QtWidgets.QMessageBox.warning(
self.view,
"Loading Pedestal Failed",
"Loading Pedestal failed make sure the file is in the valid .npy format",
QtWidgets.QMessageBox.Ok,
)
self.logger.exception("Exception when loading pedestal")
else:
self.logger.info(f'loaded Pedestal from {response[0]}')
self.updateLabelPedestalFrames(True)
def togglePedestalRecord(self):
"""
slot function for pedestal record radio button
toggle pedestal record variable and disables the frames spinboxes in acquisition tab or plot tab depenging on
the mode
"""
self.pedestalRecord = not self.pedestalRecord
def togglePedestalApply(self):
"""
slot function for pedestal apply radio button
"""
self.pedestalApply = not self.pedestalApply
def resetPedestal(self):
"""
slot function for resetting the pedestal
"""
recordOrApplyPedestal.reset(self)
def updateLabelPedestalFrames(self, loadedPedestal=False):
"""
updates labelPedestalFrames to the length of savedFrames
"""
if loadedPedestal:
self.view.labelPedestalFrames.setText('using loaded pedestal file')
else:
self.view.labelPedestalFrames.setText(f'recorded frames: {recordOrApplyPedestal.getFramesCount()}')
def subscribeToggleLegend(self, fn_cbk):
"""
subscribe to the event of toggling the hide legend checkbox by subscribing
with a callback function
"""
self.hideLegendObservers.append(fn_cbk)
def toggleLegend(self):
"""
notify subscribers for the showLegend checkbox event by executing their callbacks
"""
self.mainWindow.showLegend = not self.mainWindow.showLegend
for notify_function in self.hideLegendObservers:
notify_function()
def setCmin(self, value=None):
"""
slot for setting cmin from cminSpinBox
also used as a normal function
"""
if value is None:
self.cmin = self.view.cminSpinBox.value()
else:
self.cmin = value
self.updateColorRangeUI()
def setCmax(self, value=None):
"""
slot for setting cmax from cmaxSpinBox
also used as a normal function
"""
if value is None:
self.cmax = self.view.cmaxSpinBox.value()
else:
self.cmax = value
self.updateColorRangeUI()
def setColorRangeMode(self, mode):
"""
slot for setting the color range mode (all,fixed,3-97%)
"""
self.colorRangeMode = mode
# disable or enable cmin/cmax spinboxes
enableSpinBoxes = mode == Defines.colorRange.fixed
self.view.cminSpinBox.setEnabled(enableSpinBoxes)
self.view.cmaxSpinBox.setEnabled(enableSpinBoxes)
self.updateColorRange()
self.updateColorRangeUI()
def handleHistogramChange(self, plot):
"""
slot called after changing the color bar
This is called even when pyqtgraph sets the image without any user intervention
the class attribute ignore_histogram_signal is set to False before setting the image
so that we can distinguish between the signal originates from pyqt or from the user
"""
if not self.ignoreHistogramSignal:
self.cmin, self.cmax = plot.getHistogramWidget().item.getLevels()
self.setCmin(self.cmin)
self.setCmax(self.cmax)
self.ignoreHistogramSignal = False
# self.setColorRangeMode(Defines.colorRange.fixed)
def setFrameLimits(self, frame):
"""
function called from AcquisitionTab::read_zmq to get the maximum and minimum
values of the decoded frame and save them in frame_min/frame_max
"""
self.frame_min = np.min(frame)
self.frame_max = np.max(frame)
self.updateColorRange()
def updateColorRange(self):
"""
for mode:
- all: sets cmin and cmax to the maximums/minimum values of the frame
- 3-97%: limits cmax and cmin so that we ignore 3% from each end of the whole range
- fixed: this function does not change cmin and cmax
"""
if self.colorRangeMode == Defines.colorRange.all:
self.cmin = self.frame_min
self.cmax = self.frame_max
elif self.colorRangeMode == Defines.colorRange.center:
self.cmin = self.frame_min + 0.03 * (self.frame_max - self.frame_min)
self.cmax = self.frame_max - 0.03 * (self.frame_max - self.frame_min)
self.updateColorRangeUI()
def updateColorRangeUI(self):
"""
updates UI views should be called after every change to cmin or cmax
"""
for plot in self.imagePlots:
plot.getHistogramWidget().item.setLevels(min=self.cmin, max=self.cmax)
self.view.cminSpinBox.setValue(self.cmin)
self.view.cmaxSpinBox.setValue(self.cmax)
def setColorMap(self):
cm = pg.colormap.getFromMatplotlib(self.view.comboBoxColorMap.currentText())
# print(f'color map:{self.comboBoxColorMap.currentText()}')
self.mainWindow.plotAnalogImage.setColorMap(cm)
self.mainWindow.plotDigitalImage.setColorMap(cm)
self.mainWindow.plotTransceiverImage.setColorMap(cm)
def getZMQHWM(self):
self.view.comboBoxZMQHWM.currentIndexChanged.disconnect()
rx_zmq_hwm = self.det.getRxZmqHwm()[0]
# ensure same value in client zmq
self.det.setClientZmqHwm(rx_zmq_hwm)
# high readout, low HWM
if -1 < rx_zmq_hwm < 25:
self.view.comboBoxZMQHWM.setCurrentIndex(1)
# low readout, high HWM
else:
self.view.comboBoxZMQHWM.setCurrentIndex(0)
self.view.comboBoxZMQHWM.currentIndexChanged.connect(self.setZMQHWM)
def setZMQHWM(self):
val = self.view.comboBoxZMQHWM.currentIndex()
# low readout, high HWM
if val == 0:
self.det.setRxZmqHwm(Defines.Zmq_hwm_low_speed)
self.det.setClientZmqHwm(Defines.Zmq_hwm_low_speed)
# high readout, low HWM
else:
self.det.setRxZmqHwm(Defines.Zmq_hwm_high_speed)
self.det.setClientZmqHwm(Defines.Zmq_hwm_high_speed)
self.getZMQHWM()
def addSelectedAnalogPlots(self, i):
enable = getattr(self.adcTab.view, f"checkBoxADC{i}Plot").isChecked()
if enable:
self.mainWindow.analogPlots[i].show()
if not enable:
self.mainWindow.analogPlots[i].hide()
def addAllSelectedAnalogPlots(self):
for i in range(Defines.adc.count):
self.addSelectedAnalogPlots(i)
def removeAllAnalogPlots(self):
for i in range(Defines.adc.count):
self.mainWindow.analogPlots[i].hide()
cm = pg.colormap.get('CET-L9') # prepare a linear color map
self.mainWindow.plotDigitalImage.setColorMap(cm)
def addSelectedDigitalPlots(self, i):
enable = getattr(self.signalsTab.view, f"checkBoxBIT{i}Plot").isChecked()
if enable:
self.mainWindow.digitalPlots[i].show()
if not enable:
self.mainWindow.digitalPlots[i].hide()
def addAllSelectedDigitalPlots(self):
for i in range(Defines.signals.count):
self.addSelectedDigitalPlots(i)
def removeAllDigitalPlots(self):
for i in range(Defines.signals.count):
self.mainWindow.digitalPlots[i].hide()
def addSelectedTransceiverPlots(self, i):
enable = getattr(self.transceiverTab.view, f"checkBoxTransceiver{i}Plot").isChecked()
if enable:
self.mainWindow.transceiverPlots[i].show()
if not enable:
self.mainWindow.transceiverPlots[i].hide()
def addAllSelectedTransceiverPlots(self):
for i in range(Defines.transceiver.count):
self.addSelectedTransceiverPlots(i)
def removeAllTransceiverPlots(self):
for i in range(Defines.transceiver.count):
self.mainWindow.transceiverPlots[i].hide()
def showPlot(self):
self.mainWindow.plotAnalogWaveform.hide()
self.mainWindow.plotDigitalWaveform.hide()
self.mainWindow.plotTransceiverWaveform.hide()
self.mainWindow.plotAnalogImage.hide()
self.mainWindow.plotDigitalImage.hide()
self.mainWindow.plotTransceiverImage.hide()
self.view.labelDigitalWaveformOption.setDisabled(True)
self.view.radioButtonOverlay.setDisabled(True)
self.view.radioButtonStripe.setDisabled(True)
if self.mainWindow.romode.value in [0, 2]:
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotAnalogWaveform.show()
elif self.view.radioButtonImage.isChecked():
self.mainWindow.plotAnalogImage.show()
if self.mainWindow.romode.value in [1, 2, 4]:
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotDigitalWaveform.show()
elif self.view.radioButtonImage.isChecked():
self.mainWindow.plotDigitalImage.show()
self.view.labelDigitalWaveformOption.setEnabled(True)
self.view.radioButtonOverlay.setEnabled(True)
self.view.radioButtonStripe.setEnabled(True)
if self.mainWindow.romode.value in [3, 4]:
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotTransceiverWaveform.show()
elif self.view.radioButtonImage.isChecked():
self.mainWindow.plotTransceiverImage.show()
def plotOptions(self):
self.mainWindow.framePatternViewer.hide()
self.showPlot()
# disable plotting
self.mainWindow.read_timer.stop()
if self.view.radioButtonWaveform.isChecked():
self.mainWindow.plotAnalogWaveform.setLabel(
'left', "Output [ADC]")
self.mainWindow.plotAnalogWaveform.setLabel(
'bottom', "Analog Sample [#]")
self.mainWindow.plotDigitalWaveform.setLabel(
'left', "Digital Bit")
self.mainWindow.plotDigitalWaveform.setLabel(
'bottom', "Digital Sample [#]")
self.mainWindow.plotTransceiverWaveform.setLabel(
'left', "Transceiver Bit")
self.mainWindow.plotTransceiverWaveform.setLabel(
'bottom', "Transceiver Sample [#]")
self.view.stackedWidgetPlotType.setCurrentIndex(0)
elif self.view.radioButtonImage.isChecked():
self.view.stackedWidgetPlotType.setCurrentIndex(2)
self.setPixelMap()
if self.view.radioButtonNoPlot.isChecked():
self.view.labelPlotOptions.hide()
self.view.stackedWidgetPlotType.hide()
# enable plotting
else:
self.view.labelPlotOptions.show()
self.view.stackedWidgetPlotType.show()
self.mainWindow.read_timer.start(Defines.Time_Plot_Refresh_ms)
def setPixelMap(self):
if self.view.comboBoxPlot.currentText() == "Matterhorn02":
print("Setting pixel map for Matterhorn02")
self.mainWindow.nTransceiverRows = Defines.Matterhorn02.nRows
self.mainWindow.nTransceiverCols = Defines.Matterhorn02.nCols
self.mainWindow.pixel_map = pm.matterhorn_transceiver()
elif self.view.comboBoxPlot.currentText() == "Matterhorn1_16bit_1_counter":
print("Setting pixel map for Matterhorn1")
self.mainWindow.nTransceiverRows = Defines.Matterhorn1.nRows
self.mainWindow.nTransceiverCols = Defines.Matterhorn1.nCols
self.mainWindow.pixel_map = pm.matterhorn1_transceiver_16bit_1_counter()
elif self.view.comboBoxPlot.currentText() == "Matterhorn1_16bit_4_counters":
print("Setting pixel map for Matterhorn1 with 4 counters")
self.mainWindow.nTransceiverRows = Defines.Matterhorn1.nRows*4
self.mainWindow.nTransceiverCols = Defines.Matterhorn1.nCols
self.mainWindow.pixel_map = pm.matterhorn1_transceiver_16bit_4_counters()
elif self.view.comboBoxPlot.currentText() == "Moench04":
self.mainWindow.nAnalogRows = Defines.Moench04.nRows
self.mainWindow.nAnalogCols = Defines.Moench04.nCols
def showPatternViewer(self, enable):
if enable:
self.mainWindow.framePatternViewer.show()
self.mainWindow.framePlot.hide()
elif self.mainWindow.framePatternViewer.isVisible():
self.mainWindow.framePatternViewer.hide()
self.mainWindow.framePlot.show()
self.showPlot()
def setSerialOffset(self):
print("plot options - Not implemented yet")
# TODO:
def setNCounter(self):
print("plot options - Not implemented yet")
# TODO:
def setDynamicRange(self):
print("plot options - Not implemented yet")
# TODO:
def setImageX(self):
print("plot options - Not implemented yet")
# TODO:
def setImageY(self):
print("plot options - Not implemented yet")
# TODO:
def setRawData(self):
print("plot options - Not implemented yet")
# TODO: raw data, min, max
def setPedestalSubtract(self):
print("plot options - Not implemented yet")
# TODO: pedestal, min, max
def setFitADC(self):
print("plot options - Not implemented yet")
# TODO:
def setPlotBit(self):
print("plot options - Not implemented yet")
# TODO:
def getRandomColor(self):
'''
Returns a random color range (except white) in format string eg. "#aabbcc"
'''
randomColor = random.randrange(0, 0xffffaa, 0xaa)
return "#{:06x}".format(randomColor)
def getActiveColor(self, button):
return button.palette().color(QtGui.QPalette.Window)
def setActiveColor(self, button, str_color):
button.setStyleSheet(":enabled {background-color: %s" % str_color + "} :disabled {background-color: grey}")
def showPalette(self, button):
color = QtWidgets.QColorDialog.getColor()
if color.isValid():
self.setActiveColor(button, color.name())
# get the RGB Values
# print(color.getRgb())
def showPlotValues(self, sender, pos):
x = sender.getImageItem().mapFromScene(pos).x()
y = sender.getImageItem().mapFromScene(pos).y()
val = 0
nMaxY = self.mainWindow.nAnalogRows
nMaxX = self.mainWindow.nAnalogCols
frame = self.mainWindow.analog_frame
if sender == self.mainWindow.plotDigitalImage:
nMaxY = self.mainWindow.nDigitalRows
nMaxX = self.mainWindow.nDigitalCols
frame = self.mainWindow.digital_frame
elif sender == self.mainWindow.plotTransceiverImage:
nMaxY = self.mainWindow.nTransceiverRows
nMaxX = self.mainWindow.nTransceiverCols
frame = self.mainWindow.transceiver_frame
if 0 <= x < nMaxX and 0 <= y < nMaxY and not np.array_equal(frame, []):
val = frame[int(x), int(y)]
message = f'[{x:.2f}, {y:.2f}] = {val:.2f}'
sender.setToolTip(message)
# print(message)
else:
sender.setToolTip('')
def saveParameters(self):
commands = []
if self.view.comboBoxZMQHWM.currentIndex() == 0:
commands.append(f"zmqhwm {Defines.Zmq_hwm_low_speed}")
else:
commands.append(f"zmqhwm {Defines.Zmq_hwm_high_speed}")
return commands