class PreprocessWidget(QSplitter): def __init__(self, headermodel, selectionmodel): super(PreprocessWidget, self).__init__() self.headermodel = headermodel self.mapselectmodel = selectionmodel self.selectMapidx = 0 self.resultDict = {} self.isBatchProcessOn = False self.out = None self.dfDict = None self.reportList = ['preprocess_method', 'wav_anchor', 'interp_method', 'w_regions'] self.arrayList = ['kohlerDebased', 'kohlerBaseline', 'rubberDebased', 'deriv2_kohler', 'deriv2_rubber'] self.mousePosList = [] # split between spectrum parameters and viewwindow, vertical split self.params_and_specview = QSplitter() self.params_and_specview.setOrientation(Qt.Vertical) # split between buttons and parameters self.buttons_and_params = QSplitter() self.buttons_and_params.setOrientation(Qt.Horizontal) # split between speclist and report self.speclist_and_report = QSplitter() self.speclist_and_report.setOrientation(Qt.Vertical) # buttons layout self.buttons = QWidget() self.buttonlayout = QGridLayout() self.buttons.setLayout(self.buttonlayout) # set up buttons self.fontSize = 12 font = QFont("Helvetica [Cronyx]", self.fontSize) self.loadBtn = QPushButton() self.loadBtn.setText('Load spectra') self.loadBtn.setFont(font) self.removeBtn = QPushButton() self.removeBtn.setText('Remove spectrum') self.removeBtn.setFont(font) self.normBox = QComboBox() self.normBox.addItems(['Raw spectrum', 'Kohler EMSC baseline', 'Rubberband baseline', 'Kohler EMSC + 2nd derivative', 'Rubberband + 2nd derivative', ]) self.normBox.setFont(font) self.batchBtn = QPushButton() self.batchBtn.setText('Batch process') self.batchBtn.setFont(font) self.saveResultBox = QComboBox() self.saveResultBox.addItems(['Save kohler', 'Save kohler baseline', 'Save rubberband', 'Save kohler 2nd derivative', 'Save rubberband 2nd derivative', 'Save all', ]) self.saveResultBox.setFont(font) # add all buttons self.buttonlayout.addWidget(self.loadBtn) self.buttonlayout.addWidget(self.removeBtn) self.buttonlayout.addWidget(self.normBox) self.buttonlayout.addWidget(self.batchBtn) self.buttonlayout.addWidget(self.saveResultBox) # define report self.reportWidget = QWidget() self.reportWidget.setLayout(QVBoxLayout()) self.infoBox = QTextEdit() reportTitle = QLabel('Preprocess results') reportTitle.setFont(font) self.reportWidget.layout().addWidget(reportTitle) self.reportWidget.layout().addWidget(self.infoBox) # spectrum list view self.specItemModel = QStandardItemModel() self.specSelectModel = QItemSelectionModel(self.specItemModel) self.speclistview = QListView() self.speclistview.setModel(self.specItemModel) self.speclistview.setSelectionModel(self.specSelectModel) # add title to list view self.specListWidget = QWidget() self.listLayout = QVBoxLayout() self.specListWidget.setLayout(self.listLayout) specListTitle = QLabel('Spectrum List') specListTitle.setFont(font) self.listLayout.addWidget(specListTitle) self.listLayout.addWidget(self.speclistview) # spectrum plot self.rawSpectra = baselinePlotWidget() self.resultSpectra = baselinePlotWidget() # ParameterTree self.parametertree = PreprocessParameters() self.parameter = self.parametertree.parameter self.processArgs = self.parametertree.processArgs self.argMap = self.parametertree.argMap # assemble widgets self.buttons_and_params.addWidget(self.parametertree) self.buttons_and_params.addWidget(self.buttons) self.buttons_and_params.setSizes([1000, 100]) self.params_and_specview.addWidget(self.buttons_and_params) self.params_and_specview.addWidget(self.rawSpectra) self.params_and_specview.addWidget(self.resultSpectra) self.params_and_specview.setSizes([150, 50, 50]) self.speclist_and_report.addWidget(self.specListWidget) self.speclist_and_report.addWidget(self.reportWidget) self.speclist_and_report.setSizes([150, 100]) self.addWidget(self.params_and_specview) self.addWidget(self.speclist_and_report) self.setSizes([1000, 200]) # Connect signals self.loadBtn.clicked.connect(self.loadData) self.removeBtn.clicked.connect(self.removeSpec) self.batchBtn.clicked.connect(self.batchProcess) self.saveResultBox.currentIndexChanged.connect(self.saveResults) self.specSelectModel.selectionChanged.connect(self.updateSpecPlot) self.normBox.currentIndexChanged.connect(self.updateSpecPlot) self.parametertree.sigParamChanged.connect(self.updateSpecPlot) self.rawSpectra.scene().sigMouseClicked.connect(self.setAnchors) self.parameter.child('Preprocess method').sigValueChanged.connect(self.updateMethod) def setHeader(self, field: str): self.headers = [self.headermodel.item(i).header for i in range(self.headermodel.rowCount())] self.field = field self.wavenumberList = [] self.rc2indList = [] self.ind2rcList = [] self.pathList = [] self.dataSets = [] # get wavenumbers, rc2ind for header in self.headers: dataEvent = next(header.events(fields=[field])) self.wavenumberList.append(dataEvent['wavenumbers']) self.rc2indList.append(dataEvent['rc_index']) self.ind2rcList.append(dataEvent['index_rc']) self.pathList.append(dataEvent['path']) # get raw spectra data = None try: # spectra datasets data = header.meta_array('spectra') except IndexError: msg.logMessage('Header object contained no frames with field ''{field}''.', msg.ERROR) if data is not None: self.dataSets.append(data) def isMapOpen(self): if not self.mapselectmodel.selectedIndexes(): # no map is open return False else: self.selectMapidx = self.mapselectmodel.selectedIndexes()[0].row() return True def setAnchors(self, event): # get current map idx and selected spectrum idx specidx = self.getCurrentSpecid() plotChoice = self.normBox.currentIndex() if (not self.isMapOpen()) or (self.specItemModel.rowCount() == 0) or (specidx is None) or (plotChoice not in [2, 4]): return pos = event.pos() button = event.button() parser = Preprocessor(self.wavenumberList[self.selectMapidx], self.dataSets[self.selectMapidx][specidx]) parser.parse_anchors(self.parameter['Anchor points']) anchor_low, anchor_high = parser.wav_anchor[0], parser.wav_anchor[-1] if self.rawSpectra.getViewBox().sceneBoundingRect().contains(pos): mousePoint = self.rawSpectra.getViewBox().mapToView(pos) x = mousePoint.x() if anchor_low < x < anchor_high: if button == Qt.LeftButton:# left click, add point to mousePosList self.mousePosList.append(x) elif (button == Qt.MidButton) and self.mousePosList :# right click, remove last point from mousePosList self.mousePosList.pop() # set anchors list anchors = [anchor_low] + sorted(self.mousePosList) + [anchor_high] txt = ', '.join([str(int(round(x))) for x in anchors]) self.parameter.child('Anchor points').setValue(txt) def getCurrentSpecid(self): # get selected spectrum idx specidx = None # default value if self.specSelectModel.selectedIndexes(): selectedSpecRow = self.specSelectModel.selectedIndexes()[0].row() currentSpecItem = self.specItemModel.item(selectedSpecRow) specidx = currentSpecItem.idx return specidx def updateMethod(self): if self.parameter["Preprocess method"] == 'Kohler_EMSC': self.normBox.setCurrentIndex(1) else: self.normBox.setCurrentIndex(2) def updateSpecPlot(self): # get current map idx and selected spectrum idx specidx = self.getCurrentSpecid() if not self.isMapOpen(): return elif self.specItemModel.rowCount() == 0: MsgBox('No spectrum is loaded.\nPlease click "Load spectra" to import data.') return elif specidx is None: return # get plotchoice plotChoice = self.normBox.currentIndex() # create Preprocessor object self.out = Preprocessor(self.wavenumberList[self.selectMapidx], self.dataSets[self.selectMapidx][specidx]) baselineOK = self.out.rubber_band(**self.processArgs) and self.out.kohler(**self.processArgs) if not baselineOK: return # make results report if plotChoice != 0: self.getReport(self.out, plotChoice) # if not batch processing, show plots if not self.isBatchProcessOn: # clean up plots self.rawSpectra.clearAll() self.resultSpectra.clearAll() if plotChoice == 0: # plot raw spectrum self.infoBox.setText('') # clear txt self.rawSpectra.plotBase(self.out, plotType='raw') elif plotChoice == 1: # plot raw, kohler self.rawSpectra.plotBase(self.out, plotType='kohler_base') self.resultSpectra.plotBase(self.out, plotType='kohler') elif plotChoice == 2: # plot raw, rubberband self.rawSpectra.plotBase(self.out, plotType='rubber_base') self.resultSpectra.plotBase(self.out, plotType='rubberband') elif plotChoice == 3: # plot raw, kohler 2nd derivative self.rawSpectra.plotBase(self.out, plotType='kohler_base') self.resultSpectra.plotBase(self.out, plotType='deriv2_kohler') elif plotChoice == 4: # plot raw, rubberband 2nd derivative self.rawSpectra.plotBase(self.out, plotType='rubber_base') self.resultSpectra.plotBase(self.out, plotType='deriv2_rubberband') if plotChoice in [1, 3]: self.parameter.child('Preprocess method').setValue('Kohler_EMSC', blockSignal=self.updateMethod) elif plotChoice in [2, 4]: self.parameter.child('Preprocess method').setValue('Rubberband', blockSignal=self.updateMethod) def getReport(self, output, plotChoice): resultTxt = '' # get baseline results reportList = self.reportList.copy() if plotChoice in [2, 4]: reportList = self.reportList[:-1] output.preprocess_method = 'rubberband' elif plotChoice in [1, 3]: reportList = [self.reportList[0], self.reportList[-1]] output.preprocess_method = 'kohler' for item in dir(output): if item in reportList: if item == 'wav_anchor': val = getattr(output, item) printFormat = ('{:.2f}, ' * len(val))[:-1] resultTxt += item + ': ' + printFormat.format(*val) + '\n' else: resultTxt += item + ': ' + str(getattr(output, item)) + '\n' if (item in self.arrayList) or (item in self.reportList): self.resultDict[item] = getattr(output, item) # send text to report info box self.infoBox.setText(resultTxt) def loadData(self): # get current map idx if not self.isMapOpen(): return # pass the selected map data to plotwidget self.rawSpectra.setHeader(self.headers[self.selectMapidx], 'spectra') currentMapItem = self.headermodel.item(self.selectMapidx) rc2ind = self.rc2indList[self.selectMapidx] # get current map name mapName = currentMapItem.data(0) # get current selected pixels pixelCoord = currentMapItem.selectedPixels # get selected specIds spectraIds = [] if currentMapItem.selectedPixels is None: # select all spectraIds = list(range(len(rc2ind))) else: for i in range(len(pixelCoord)): row_col = tuple(pixelCoord[i]) spectraIds.append(rc2ind[row_col]) spectraIds = sorted(spectraIds) # add specitem model self.specItemModel.clear() for idx in spectraIds: item = QStandardItem(mapName + '# ' + str(idx)) item.idx = idx self.specItemModel.appendRow(item) def removeSpec(self): # get current selectedSpecRow if self.specSelectModel.selectedIndexes(): selectedSpecRow = self.specSelectModel.selectedIndexes()[0].row() self.specSelectModel.blockSignals(True) self.specItemModel.removeRow(selectedSpecRow) self.specSelectModel.blockSignals(False) # clean up plots self.rawSpectra.clearAll() self.resultSpectra.clearAll() self.infoBox.setText('') def cleanUp(self): self.specItemModel.clear() self.rawSpectra.clearAll() self.resultSpectra.clearAll() self.infoBox.setText('') self.mousePosList = [] self.normBox.setCurrentIndex(0) def batchProcess(self): # get current map idx if not self.isMapOpen(): return elif self.specItemModel.rowCount() == 0: MsgBox('No spectrum is loaded.\nPlease click "Load spectra" to import data.') return # check if baseline fit OK if self.out is None: self.out = Preprocessor(self.wavenumberList[self.selectMapidx], self.dataSets[self.selectMapidx][0]) # get plotchoice plotChoice = self.normBox.currentIndex() if plotChoice != 0: # calculate rubberband and kohler baseline baselineOK = self.out.rubber_band(**self.processArgs) and self.out.kohler(**self.processArgs) else: MsgBox('Plot type is "Raw spectrum".\nPlease change plot type to "Kohler" or "Rubberband".') return if not baselineOK: return # notice to user userMsg = YesNoDialog(f'Ready to batch process selected spectra.\nDo you want to continue?') userChoice = userMsg.choice() if userChoice == QMessageBox.No: # user choose to stop return self.isBatchProcessOn = True # init resultSetsDict, paramsDict self.resultSetsDict = {} self.paramsDict = {} self.paramsDict['specID'] = [] self.paramsDict['row_column'] = [] ind2rc = self.ind2rcList[self.selectMapidx] energy = self.out.energy n_energy = len(energy) for item in self.arrayList: self.resultSetsDict[item] = np.empty((0, n_energy)) for item in self.reportList: self.paramsDict[item] = [] # batch process begins n_spectra = self.specItemModel.rowCount() for i in range(n_spectra): msg.showMessage(f'Processing {i + 1}/{n_spectra} spectra') # select each spec and collect results self.specSelectModel.select(self.specItemModel.index(i, 0), QItemSelectionModel.ClearAndSelect) # get spec idx currentSpecItem = self.specItemModel.item(i) self.paramsDict['specID'].append(currentSpecItem.idx) self.paramsDict['row_column'].append(ind2rc[currentSpecItem.idx]) # append all results into a single array/list for item in self.arrayList: self.resultSetsDict[item] = np.append(self.resultSetsDict[item], self.resultDict[item].reshape(1, -1), axis=0) for item in self.reportList: self.paramsDict[item].append(self.resultDict[item]) # result collection completed. convert paramsDict to df self.dfDict = {} self.dfDict['param'] = pd.DataFrame(self.paramsDict).set_index('specID') for item in self.arrayList: # convert resultSetsDict to df self.dfDict[item] = pd.DataFrame(self.resultSetsDict[item], columns=energy.tolist(), index=self.paramsDict['specID']) # batch process completed self.isBatchProcessOn = False msg.showMessage(f'Batch processing is completed! Saving results to csv files.') # save df to files self.saveResults() def saveResults(self): if self.dfDict is None: return filePath = self.pathList[self.selectMapidx] energy = self.out.energy saveDataChoice = self.saveResultBox.currentIndex() if saveDataChoice != 5: # save a single result saveDataType = self.arrayList[saveDataChoice] dirName, csvName, h5Name = self.saveToFiles(energy, self.dfDict, filePath, saveDataType) if h5Name is None: MsgBox(f'Processed data was saved as csv file at: \n{dirName + csvName}') else: MsgBox( f'Processed data was saved as: \n\ncsv file at: {dirName + csvName} and \n\nHDF5 file at: {dirName + h5Name}') else: # save all results csvList = [] h5List = [] for saveDataType in self.arrayList: dirName, csvName, h5Name = self.saveToFiles(energy, self.dfDict, filePath, saveDataType) csvList.append(csvName) h5List.append(h5Name) allcsvName = (', ').join(csvList) if h5Name is None: MsgBox(f'Processed data was saved as csv files at: \n{dirName + allcsvName}') else: allh5Name = (', ').join(h5List) MsgBox( f'Processed data was saved as: \n\ncsv files at: {dirName + allcsvName} and \n\nHDF5 files at: {dirName + allh5Name}') # save parameter xlsName = csvName[:-4] + '_param.xlsx' self.dfDict['param'].to_excel(dirName + xlsName) def saveToFiles(self, energy, dfDict, filePath, saveDataType): ind2rc = self.ind2rcList[self.selectMapidx] n_spectra = self.specItemModel.rowCount() # get dirname and old filename dirName = os.path.dirname(filePath) dirName += '/' oldFileName = os.path.basename(filePath) # save dataFrames to csv file csvName = oldFileName[:-3] + '_' + saveDataType + '.csv' dfDict[saveDataType].to_csv(dirName + csvName) # if a full map is processed, also save results to a h5 file h5Name = None if n_spectra == len(ind2rc): fullMap = ir_map(filename=filePath) fullMap.add_image_cube() fullMap.wavenumbers = energy fullMap.N_w = len(energy) fullMap.data = np.zeros((fullMap.data.shape[0], fullMap.N_w)) fullMap.imageCube = np.zeros((fullMap.imageCube.shape[0], fullMap.imageCube.shape[1], fullMap.N_w)) for i in self.paramsDict['specID']: fullMap.data[i, :] = self.resultSetsDict[saveDataType][i, :] row, col = ind2rc[i] fullMap.imageCube[row, col, :] = fullMap.data[i, :] = self.resultSetsDict[saveDataType][i, :] # save data as hdf5 h5Name = oldFileName[:-3] + '_' + saveDataType + '.h5' fullMap.write_as_hdf5(dirName + h5Name) return dirName, csvName, h5Name
class FrequencySelection(QGroupBox): """ frequency selection """ def __init__(self, parent, show_period=True, show_frequency=True, allow_range_select=True, select_multiple=True): QGroupBox.__init__(self, parent) self._mt_objs = None self._unique_periods = None self._unique_frequencies = None self._periods = None self._frequencies = None self._allow_range = allow_range_select self._select_multiple = select_multiple self.ui = Ui_GroupBox_frequency_select() self.ui.setupUi(self) self.ui.label_place_holder.hide() self.model_selected = QStandardItemModel() self.ui.listView_selected.setModel(self.model_selected) self.frequency_delegate = FrequencySelection.FrequencyDelegate(self.ui.listView_selected) self.ui.listView_selected.setItemDelegate(self.frequency_delegate) self.histogram = FrequencySelection.Histogram(self, allow_range_select=self._allow_range) self.histogram.set_unit(self._units[0]) self.histogram.set_tol(self.ui.doubleSpinBox_tolerance.value()) self.histogram.frequency_selected.connect(self._frequency_selected) self.histogram.frequency_range_selected.connect(self._frequency_selected) self.ui.widget_histgram.layout().addWidget(self.histogram) self.ui.radioButton_period.setChecked(show_period) self.ui.radioButton_frequency.setChecked(show_frequency) self.ui.doubleSpinBox_tolerance.setHidden(not self._allow_range) self.ui.checkBox_existing_only.setChecked(not self._allow_range) self.ui.checkBox_existing_only.setHidden(not self._allow_range) self.ui.label_tolerance.setHidden(not self._allow_range) self.ui.radioButton_period.setHidden(not (show_period and show_frequency)) self.ui.radioButton_frequency.setHidden(not (show_period and show_frequency)) if self.ui.radioButton_frequency.isHidden(): self.setTitle(self._type[1]) elif self.ui.radioButton_period.isHidden(): self.setTitle(self._type[0]) self.ui.radioButton_frequency.toggled.connect(self._frequency_toggled) self.ui.checkBox_existing_only.toggled.connect(self.histogram.select_existing) self.ui.checkBox_existing_only.toggled.connect(self.model_selected.clear) self.ui.checkBox_show_existing.toggled.connect(self.histogram.show_existing) self.ui.checkBox_x_log_scale.toggled.connect(self.histogram.set_x_log_scale) self.ui.checkBox_y_log_scale.toggled.connect(self.histogram.set_y_log_scale) self.ui.pushButton_clear.clicked.connect(self._clear_all) self.ui.pushButton_delete.clicked.connect(self._delete_selected) self.ui.doubleSpinBox_tolerance.valueChanged.connect(self.histogram.set_tol) def set_data(self, mt_objs): self._mt_objs = mt_objs self._unique_frequencies = None self._unique_periods = None self._update_frequency() def get_frequencies(self): frequencies = [self.model_selected.item(index).data(QtCore.Qt.DisplayRole) for index in range(self.model_selected.rowCount())] if self._allow_range: frequencies = [(freq[0], freq[1]) if isinstance(freq, tuple) else freq for freq in frequencies] else: frequencies = [freq[3] if isinstance(freq, tuple) else freq for freq in frequencies if (isinstance(freq, tuple) and len(freq) == 5) or isinstance(freq, float)] # print frequencies if self._select_multiple: return frequencies else: return frequencies[0] if frequencies else self._unique_frequencies[0] # if nothing selected, return minimal frequency _units = ['Hz', 's'] _type = ['Frequency', 'Period'] def _clear_all(self): self.model_selected.clear() self.histogram.clear_all_drawing() def _delete_selected(self): for item in [self.model_selected.item(index.row()) for index in self.ui.listView_selected.selectedIndexes()]: x = item.data(QtCore.Qt.DisplayRole) self.model_selected.removeRow(self.model_selected.indexFromItem(item).row()) self.histogram.remove_marker(x) def _frequency_selected(self, x): if not self._select_multiple: self.histogram.clear_all_drawing() self.model_selected.clear() for item in [self.model_selected.item(index) for index in range(self.model_selected.rowCount())]: value = item.data(QtCore.Qt.DisplayRole) if value == x: return elif isinstance(value, tuple) and isinstance(x, float) and value[0] <= x <= value[1]: return # x already in interval elif isinstance(x, tuple) and isinstance(value, float) and x[0] <= value <= x[1]: # existing value in new interval self.model_selected.removeRow(self.model_selected.indexFromItem(item).row()) self.histogram.remove_marker(value) elif isinstance(x, tuple) and isinstance(value, tuple): if min(x[1], value[1]) - max(x[0], value[0]) >= 0: # there is intersection between intervals, so marge them mi = min(x[0], value[0]) ma = max(x[1], value[1]) uniques = self._unique_frequencies \ if self.ui.radioButton_frequency.isChecked() \ else self._unique_periods num = len( [freq for freq in uniques if mi <= freq <= ma]) # num of existing freqs in the new interval x = (mi, ma, num) # remove old interval self.model_selected.removeRow(self.model_selected.indexFromItem(item).row()) self.histogram.remove_marker(value) else: prec = self.frequency_delegate.prec while np.all(np.isclose(value, x, pow(.1, prec))): prec += 1 self.frequency_delegate.prec = prec new_item = FrequencySelection.FrequencyItem() new_item.setData(x, QtCore.Qt.DisplayRole) # update graphic if isinstance(x, float): self.histogram.add_marker(x) # new_item.setData(x, QtCore.Qt.UserRole) elif isinstance(x, tuple): self.histogram.add_marker(x) # new_item.setData(x[0], QtCore.Qt.UserRole) # update model self.model_selected.appendRow(new_item) self.model_selected.sort(0) def show_period(self): self.ui.radioButton_period.setChecked(True) def show_frequency(self): self.ui.radioButton_frequency.setChecked(True) def _frequency_toggled(self, is_checked): self.histogram.set_unit(self._units[0] if is_checked else self._units[1]) self._update_frequency() def _update_frequency(self): self.model_selected.clear() if self._mt_objs is not None: if self._unique_frequencies is None: self._frequencies = [freq for mt_obj in self._mt_objs for freq in list(mt_obj.Z.freq)] all_unique = set(self._frequencies) self._unique_frequencies = sorted(list(all_unique)) if self.ui.radioButton_period.isChecked() and self._unique_periods is None: self._periods = 1. / np.array(self._frequencies) all_unique = set(self._periods) self._unique_periods = sorted(list(all_unique)) self.histogram.set_data( self._periods if self.ui.radioButton_period.isChecked() else self._frequencies, self._unique_periods if self.ui.radioButton_period.isChecked() else self._unique_frequencies ) self.frequency_delegate.freqs = self._unique_periods \ if self.ui.radioButton_period.isChecked() \ else self._unique_frequencies self.histogram.update_figure() class FrequencyItem(QStandardItem): def __lt__(self, other): value = self.data(QtCore.Qt.DisplayRole) other_value = other.data(QtCore.Qt.DisplayRole) if isinstance(value, tuple): value = value[0] if isinstance(other_value, tuple): other_value = other_value[0] return value < other_value class FrequencyDelegate(QStyledItemDelegate): _prec = 5 # decimal places def get_prec(self): return self._prec def set_prec(self, prec): self._prec = prec prec = property(get_prec, set_prec) def displayText(self, value, locale): if isinstance(value, float): return '{:.{prec}f}'.format(value, prec=self._prec) elif isinstance(value, tuple) and len(value) == 3: # (min, max, num) return '{}{}, {}{} ({num} selected)'.format( '(' if value[0] == -np.inf else '[', '{:.{prec}f}'.format(value[0], prec=self._prec), '{:.{prec}f}'.format(value[1], prec=self._prec), ')' if value[1] == np.inf else ']', num=value[2] ) elif len(value) == 5: # (min, max, num, freq, tol) return '{:.{prec}f} ±{tol}% ({num} selected)'.format( value[3], prec=self._prec, tol=value[4], num=value[2]) # elif isinstance(py_obj, set): # return '{{}}'.format(','.join(['{:.{prec}f}'.format(f, prec=self._prec) for f in py_obj if isinstance(f, float)])) return value class Histogram(MPLCanvas): def __init__(self, parent, y_log_scale=False, x_log_scale=False, allow_range_select=True): self._frequencies = None self._unique_frequencies = None self._title = None self._unit = None self._press = None self._tol = None MPLCanvas.__init__(self, parent, 5, 1.5) self._lx = {} self._cursor = None self._select_existing_only = False self._show_existing = False self._x_log_scale = x_log_scale self._y_log_scale = y_log_scale self._select_range = allow_range_select if self._select_range: self.mpl_connect('button_press_event', self.on_press) self.mpl_connect('button_release_event', self.on_release) def add_marker(self, x): if isinstance(x, float): lx = self._lx.setdefault(x, self._draw_v_line(x)) # self._axes.draw_artist(lx) self.draw_idle() elif isinstance(x, tuple): if len(x) == 3: lx = self._lx.setdefault(x, self._fill_v_area(x[0], x[1])) elif len(x) == 5: lx = self._lx.setdefault(x, ( self._draw_v_line(x[3]), self._fill_v_area(x[0], x[1]) )) else: raise NotImplemented self.draw_idle() def remove_marker(self, x): if x in self._lx: marker = self._lx[x] if isinstance(marker, tuple): for m in marker: m.remove() else: marker.remove() self.draw_idle() del self._lx[x] def clear_all_drawing(self): for key in list(self._lx.keys()): marker = self._lx[key] if isinstance(marker, tuple): for m in marker: m.remove() else: marker.remove() self._lx.clear() self.draw_idle() def set_unit(self, unit): if unit != self._unit: self._unit = unit self._cursor = Cursor(self._axes, track_y=False, show_drag=self._select_range, text_format="%f" + self._unit, useblit=True) def select_existing(self, select_existing): self._select_existing_only = select_existing self.clear_all_drawing() def set_tol(self, tol): self._tol = tol def show_existing(self, show_existing): self._show_existing = show_existing self.update_figure() def set_data(self, frequencies, unique_frequencies=None): self._frequencies = frequencies if unique_frequencies is not None: self._unique_frequencies = unique_frequencies else: self._unique_frequencies = sorted(list(set(frequencies))) self._lx.clear() def set_y_log_scale(self, ischecked): self._y_log_scale = ischecked self.update_figure() def set_x_log_scale(self, isChecked): self._x_log_scale = isChecked self.update_figure() frequency_selected = Signal(float) frequency_range_selected = Signal(tuple) def _get_valid_cursor_loc(self, event): if not event.inaxes: pos = self._axes.get_position() if self.height() * pos.y0 < event.y < self.height() * pos.y1: x = -np.inf if event.x < self.width() * pos.x0 else np.inf else: x = None else: x = event.xdata return x def on_press(self, event): self._press = self._get_valid_cursor_loc(event) def on_release(self, event): x = self._get_valid_cursor_loc(event) if x: if self._press and self._press != x: # emit (min, max, num) if self._press < x: self.frequency_range_selected.emit( ( self._press, x, len([freq for freq in self._unique_frequencies if self._press <= freq <= x]) ) ) elif self._press > x: self.frequency_range_selected.emit( ( x, self._press, len([freq for freq in self._unique_frequencies if x <= freq <= self._press]) ) ) elif not self._select_range or self._select_existing_only: x = self._find_closest(x) self.frequency_selected.emit(x) else: # emit (min, max, num, freq, tol) tol = x * self._tol / 100. min = x - tol max = x + tol self.frequency_range_selected.emit( ( min, max, len([freq for freq in self._unique_frequencies if min <= freq <= max]), x, self._tol ) ) self._press = None def _find_closest(self, x): return min(self._frequencies, key=lambda freq: abs(freq - x)) def compute_initial_figure(self): self._axes.tick_params(axis='both', which='major', labelsize=6) self._axes.tick_params(axis='both', which='minor', labelsize=4) if self._frequencies is not None: bins = gen_hist_bins(self._unique_frequencies) self._axes.hist(self._frequencies, bins=bins) # , 50, normed=1) if self._y_log_scale: self._axes.set_yscale('log', nonposy='clip') if self._x_log_scale: self._axes.set_xscale('log', nonposx='clip') if self._show_existing: for freq in self._unique_frequencies: self._axes.axvline(freq, linewidth=1, color='black', alpha=0.2) if self._title and self._unit: self._axes.set_xlabel("%s (%s)" % (self._title, self._unit), fontsize=8) self.figure.suptitle('%s Distribution in Selected Stations' % self._title, fontsize=8) self._fig.set_tight_layout(True) def update_figure(self): self._axes.cla() self.compute_initial_figure() for key in list(self._lx.keys()): if isinstance(key, float): self._lx[key] = self._draw_v_line(key) elif isinstance(key, tuple): if len(key) == 3: self._lx[key] = self._fill_v_area(key[0], key[1]) elif len(key) == 5: self._lx[key] = (self._draw_v_line(key[3]), self._fill_v_area(key[0], key[1])) self.draw() def _draw_v_line(self, x): if x == -np.inf: x = self._axes.get_xlim()[0] if x == np.inf: x = self._axes.get_xlim()[1] return self._axes.axvline(x=x, linewidth=1, color="red") def _fill_v_area(self, x1, x2): if x1 == -np.inf: x1 = self._axes.get_xlim()[0] if x2 == np.inf: x2 = self._axes.get_xlim()[1] return self._axes.axvspan(x1, x2, alpha=0.5, color='red')
class LogTable(QTreeView, PyDMWidget): """Log Table.""" updated = Signal() def __init__(self, parent=None, channels=list(), label2width=dict(), is_status=False): # QTableView.__init__(self, parent) QTreeView.__init__(self, parent) PyDMWidget.__init__(self) # setup table self._is_status = is_status self._date_fmt = ' %Y/%m/%d ' self._time_fmt = ' %H:%M:%S ' self.headerLabels = label2width.keys() self._model = QStandardItemModel() self._model.setHorizontalHeaderLabels(self.headerLabels) self.setModel(self._model) self.setUniformRowHeights(True) self.setHeader(QHeaderView(Qt.Horizontal)) for idx, width in enumerate(label2width.values()): self.header().resizeSection(idx, width) self.header().resizeSections(QHeaderView.Fixed) self.header().setStretchLastSection(True) self.setSortingEnabled(True) self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.setItemDelegateForColumn(2, LogItemDelegate(self)) self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setStyleSheet("gridline-color: #ffffff;") # set channels self.address2conn = dict() self.address2channels = dict() for address in channels: self.address2conn[address] = False channel = SiriusConnectionSignal( address=address, connection_slot=self.connection_changed, value_slot=self.value_changed, severity_slot=self.alarm_severity_changed) channel.connect() self.address2channels[address] = channel self._channels.append(channel) @Slot(bool) def connection_changed(self, conn): """Reimplement connection_changed to handle all channels.""" address = self.sender().address self.address2conn[address] = conn allconn = True for conn in self.address2conn.values(): allconn &= conn self.setState(allconn) self._connected = allconn def add_log_slot(self, updated): new_value = self._get_newitem_data(updated) if not new_value: return self.add_log(new_value) def add_log(self, new_value): if self._is_status: self.remove_log(new_value) datetime_now = _datetime.now() item_data = [QStandardItem(text) for text in ( datetime_now.date().strftime(self._date_fmt), datetime_now.time().strftime(self._time_fmt), new_value['logtype'], new_value['psname'], new_value['propty'], new_value['value'])] for item in item_data: item.setTextAlignment(Qt.AlignCenter) self._model.insertRow(0, item_data) if self._model.rowCount() > 10000: self._model.removeRow(self._model.rowCount()-1) self.updated.emit() def remove_log_slot(self, updated): new_value = self._get_newitem_data(updated) if not new_value: return self.remove_log(new_value) def remove_log(self, new_value): for row in range(self._model.rowCount()): logtype = self._model.data(self._model.index(row, 2)) if logtype != new_value['logtype']: continue psname = self._model.data(self._model.index(row, 3)) if psname != new_value['psname']: continue propty = self._model.data(self._model.index(row, 4)) if propty != new_value['propty']: continue self._model.removeRow(row) self.updated.emit() def alarm_severity_changed(self, new_alarm_severity): """Reimplement alarm_severity_changed.""" if self.sender(): pv_diff = _PVName(self.sender().address) val_diff = self.address2channels[pv_diff].value pv_opmd = pv_diff.substitute( propty_name='OpMode', propty_suffix='Sts') val_opmd = self.address2channels[pv_opmd].value is_slowref = val_opmd == _PSConst.States.SlowRef new_value = {'logtype': 'WARN', 'psname': pv_diff.device_name, 'propty': pv_diff.propty_name, 'value': str(val_diff)} if new_alarm_severity in [_Sev.MINOR_ALARM, _Sev.MAJOR_ALARM] and \ is_slowref: self.add_log(new_value) elif self._is_status: self.remove_log(new_value) super().alarm_severity_changed(new_alarm_severity) def _get_newitem_data(self, updated): pv, value = updated pv = _PVName(pv) if isinstance(value, _np.ndarray): _log.warning('PSDiag window received a numpy array to ' + pv+' ('+str(value)+')!') return if value is None: return if 'conn' in self.sender().objectName(): str_value = 'disconnected' logtype = 'DISCONN' elif pv.propty_name == 'PwrState': str_value = _PSEnums.PWRSTATE_STS[value] logtype = 'ERR' elif pv.propty_name == 'OpMode': str_value = _PSEnums.STATES[value] logtype = 'WARN' else: str_value = str(value) logtype = 'ERR' return {'logtype': logtype, 'psname': pv.device_name, 'propty': '' if logtype == 'DISCONN' else pv.propty_name, 'value': str_value} def mouseDoubleClickEvent(self, ev): """Trigger open PS detail window.""" idx = self.selectedIndexes() text = self._model.data(self._model.index(idx[0].row(), 3)) text = _PVName(text) if text.dis == 'PS': _run_newprocess(['sirius-hla-as-ps-detail.py', text]) elif text.dis == 'PU': _run_newprocess(['sirius-hla-as-pu-detail.py', text]) super().mouseDoubleClickEvent(ev)