# -*- coding: utf-8 -*- #!/usr/bin/env python3 """ GEPARD - Gepard-Enabled PARticle Detection Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Polymerforschung Dresden e. V. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ from PyQt5 import QtCore, QtGui, QtWidgets from .excelexport import ExpExcelDialog from . import analysisplots from . import analysiswidgets from .loadresults import LoadTrueMatchResults from .database import DataBaseWindow from .colorlegend import getColorFromNameWithSeed from .particleCharacterization import updateStatsOfParticlesIfNotManuallyEdited try: from .sqlexport import SQLExport sqlEnabled = True except: sqlEnabled = False class ParticleAnalysis(QtWidgets.QMainWindow): def __init__(self, dataset, viewparent=None): super(ParticleAnalysis, self).__init__(viewparent) self.setWindowTitle('Results of polymer analysis') self.layout = QtWidgets.QHBoxLayout() self.widget = QtWidgets.QWidget() self.widget.setLayout(self.layout) self.setCentralWidget(self.widget) self.viewparent = viewparent self.dataset = dataset self.particleContainer = dataset.particleContainer self.importWindow = None self.findcoloredParticlesWindow = None self.currentParticleIndex = 0 self.currentSpectrumIndex = 0 self.typeHistogramPlot = analysisplots.TypeHistogramPlot(self.dataset) self.sizeHistogramPlot = analysisplots.SizeHistogramPlot(self.dataset) self.specPlot = analysisplots.SpectraPlot(self.dataset) splitter1 = QtWidgets.QSplitter(QtCore.Qt.Vertical) splitter1.addWidget(self.specPlot) splitter1.addWidget(self.sizeHistogramPlot) splitter2 = QtWidgets.QSplitter(QtCore.Qt.Horizontal) splitter2.addWidget(splitter1) splitter2.addWidget(self.typeHistogramPlot) splitter2.setSizes([300, 150]) referenceGroup = QtWidgets.QGroupBox('Reference Spectra') referenceLayout = QtWidgets.QHBoxLayout() self.refSelector = QtWidgets.QComboBox() self.refSelector.setMinimumWidth(200) self.refSelector.setDisabled(True) self.dbWin = DataBaseWindow(self) self.dbWin.selectDataBase(refreshParent=True) #this includes updating the refSelector self.refSelector.currentIndexChanged.connect(self.updateSpecPlot) referenceLayout.addWidget(QtWidgets.QLabel('Select Reference')) referenceLayout.addWidget(self.refSelector) referenceLayout.addStretch() referenceGroup.setLayout(referenceLayout) self.navigationToolbar = analysiswidgets.PolymerNavigationToolbar(self.particleContainer) self.navigationToolbar.WidgetsUpdated.connect(self.updateFromNavigationToolbar) self.navigationToolbar.JumpToIndicatedSpec.connect(self.jumpToIndicatedSpectrum) self.viewparent.ParticleOfIndexSelected.connect(self.navigationToolbar.setWidgetsToNewParticleIndex) topLayout = QtWidgets.QHBoxLayout() topLayout.addWidget(self.navigationToolbar) topLayout.addWidget(referenceGroup) viewLayout = QtWidgets.QVBoxLayout() viewLayout.addLayout(topLayout) viewLayout.addWidget(splitter2) viewLayout.setStretch(1, 1) self.optionsGroup = QtWidgets.QGroupBox('Set HQI Threshold') optionsLayout = QtWidgets.QFormLayout() self.hqiSpinBox = QtWidgets.QDoubleSpinBox() self.hqiSpinBox.setDecimals(1) self.hqiSpinBox.setMinimum(0) self.hqiSpinBox.setMaximum(100) self.hqiSpinBox.setMaximumWidth(100) minHQI = self.dataset.resultParams['minHQI'] if minHQI is not None: self.hqiSpinBox.setValue(minHQI) self.hqiSpinBox.valueChanged.connect(self.applyHQIThresholdToResults) minHQI = self.dataset.resultParams['minHQI'] if minHQI is not None: self.hqiSpinBox.setValue(minHQI) optionsLayout.addRow(QtWidgets.QLabel('minimum HQI:'), self.hqiSpinBox) self.optionsGroup.setLayout(optionsLayout) self.optionsGroup.setMinimumWidth(175) self.resultCheckBoxes = analysiswidgets.PolymerTypeCheckboxes() self.resultCheckBoxes.PolymerCheckBoxToggled.connect(self.updatedPlotsAndContoursSelectively) self.menuLayout = QtWidgets.QVBoxLayout() self.menuLayout.addWidget(self.optionsGroup) self.menuLayout.addWidget(self.resultCheckBoxes) self.layout.addLayout(self.menuLayout) self.layout.addLayout(viewLayout) self.createActions() self.createMenus() self.applyHQIThresholdToResults() self.initializeSpecPlot() self.navigationToolbar.updateWidgets() def createActions(self): self.loadTrueMatchAct = QtWidgets.QAction("Load &TrueMatch Results", self) self.loadTrueMatchAct.triggered.connect(self.importTrueMatchResults) self.loadSpectraAct = QtWidgets.QAction("Load &Spectra", self) self.loadSpectraAct.triggered.connect(self.initializeSpecPlot) self.databaseAct = QtWidgets.QAction("&Manage reference spectra databases", self) self.databaseAct.triggered.connect(self.launchDBManager) self.findColorParticlesAct = QtWidgets.QAction("Navigate throuch &colored, not unknown particles", self) self.findColorParticlesAct.triggered.connect(self.launchColoredParticleWindow) self.recalculateParticleStatsAct = QtWidgets.QAction("&Recalculated particle stats", self) self.recalculateParticleStatsAct.triggered.connect(self.recalculateParticleStats) self.expExcelAct= QtWidgets.QAction("Export &Excel List", self) self.expExcelAct.setDisabled(True) self.expExcelAct.triggered.connect(self.exportToExcel) self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self) self.expSQLAct.setDisabled(True) self.expSQLAct.triggered.connect(self.exportToSQL) self.getAndActivateActionsFromGepardMain() def getAndActivateActionsFromGepardMain(self): """ For user convenience, the actions from the Gepard main window are also added to the menubar of the analysis window. :return: """ gepard = self.viewparent.imparent self.noOverlayAct = gepard.noOverlayAct self.selOverlayAct = gepard.selOverlayAct self.fullOverlayAct = gepard.fullOverlayAct self.transpAct = gepard.transpAct self.transpAct.triggered.connect(self.updateContourColors) self.hideLabelAct = gepard.hideLabelAct self.hideLabelAct.triggered.connect(self.showOrHideSpecNumbers) self.darkenAct = gepard.darkenAct self.darkenAct.triggered.connect(self.darkenBackground) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct]: act.setCheckable(True) self.fullOverlayAct.setChecked(True) self.seedAct = gepard.seedAct self.seedAct.triggered.connect(self.updateColorSeed) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]: act.setDisabled(False) def createMenus(self): self.importMenu = QtWidgets.QMenu("&Import Spectra and Results") self.importMenu.addActions([self.loadSpectraAct, self.loadTrueMatchAct]) self.dispMenu = QtWidgets.QMenu("&Display", self) self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu) self.overlayActGroup.setExclusive(True) self.overlayActGroup.triggered.connect(self.updateContourColors) self.overlayActGroup.triggered.connect(self.updatePlotsAndContours) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct]: self.dispMenu.addAction(act) self.overlayActGroup.addAction(act) self.dispMenu.addSeparator() self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct]) self.toolMenu = QtWidgets.QMenu("&Tools") self.toolMenu.addAction(self.databaseAct) self.toolMenu.addAction(self.findColorParticlesAct) self.toolMenu.addAction(self.recalculateParticleStatsAct) self.exportMenu = QtWidgets.QMenu("&Export", self) self.exportMenu.addAction(self.expExcelAct) self.exportMenu.addAction(self.expSQLAct) self.menuBar().addMenu(self.importMenu) self.menuBar().addMenu(self.dispMenu) self.menuBar().addMenu(self.toolMenu) self.menuBar().addMenu(self.exportMenu) def launchDBManager(self): """ The Database Manager is launched for editing and creating databases with reference spectra :return: """ if self.dbWin.isHidden(): self.dbWin.show() def launchColoredParticleWindow(self): """ Likely just a temporary function to find colored particles, as the color classification is still not perfect. The module finds not unknown particles that were assigned to a color and the user can confirm or change the respective colors. :return: """ if self.findcoloredParticlesWindow is not None: del self.findcoloredParticlesWindow self.findcoloredParticlesWindow = analysiswidgets.FindColoredParticleWindow(self) self.findcoloredParticlesWindow.ParticleOfIndexSelected.connect(self.navigationToolbar.setWidgetsToNewParticleIndex) def recalculateParticleStats(self): """ recalculate all particle colors, if not already manually set """ self.setDisabled(True) self.viewparent.blockUI() reply = QtWidgets.QMessageBox.question(self, 'Recalculate stats', "Do you want recalculate stats of all particles?\nThis might take a while..", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: updateStatsOfParticlesIfNotManuallyEdited(self.particleContainer) self.updatedPlotsAndContoursSelectively() QtWidgets.QMessageBox.about(self, 'Done', 'Recalculation is finished') self.viewparent.unblockUI() self.setDisabled(False) def populateRefSelector(self): """ The Combobox for selecting references is updated to the currently selected database :return: """ self.refSelector.clear() if self.dbWin.activeDatabase is None: self.refSelector.setDisabled(True) else: self.refSelector.addItem('') self.refSelector.addItems(self.dbWin.activeDatabase.spectraNames) self.refSelector.setDisabled(False) def importTrueMatchResults(self): if self.importWindow is not None: del self.importWindow self.importWindow = LoadTrueMatchResults(self.particleContainer, self) @QtCore.pyqtSlot() def applyHQIThresholdToResults(self): """ The in the hqiSpinBox indicated value is applied to all spectra in the dataset. All plots are updated, as the assignment of each particle can have changed. :return: """ hqi = self.hqiSpinBox.value() self.particleContainer.applyHQITresholdToParticles(hqi) self.dataset.resultParams['minHQI'] = hqi self.dataset.save() self.updateWidgetContents() self.updatePlotsAndContours() def updatePlotsAndContours(self): """ All plots are updated, the colors of the contourItems in the sampleview are updated :return: """ self.updateTypeHistogram() self.updateSizeHistogram() self.updateContourColors() self.updateLegend() @QtCore.pyqtSlot() def updatedPlotsAndContoursSelectively(self): """ ONLY if a selection of polymers is to be displayed, TypeHistogram, contours and legend are updated. Otherwise only the sizeHistogram needs to be updated :return: """ self.updateSizeHistogram() if self.selOverlayAct.isChecked(): self.updateTypeHistogram() self.updateContourColors() self.updateLegend() def updateWidgetContents(self): """ The content of all child widgets is updated to a new status in the particleContainer :return: """ uniquePolymers = self.particleContainer.getUniquePolymers() self.resultCheckBoxes.updatePolymerCheckBoxes(uniquePolymers) self.navigationToolbar.updateWidgets() self.expExcelAct.setDisabled(False) if sqlEnabled: self.expSQLAct.setDisabled(False) def initializeSpecPlot(self): self.specPlot.loadSpectraAndInitializeSpecPlot() self.updateSpecPlot() def updateTypeHistogram(self): if self.particleContainer.getNumberOfParticles() > 0: if self.selOverlayAct.isChecked(): histogramData = self.getSelectedTypeHistogram() else: histogramData = self.particleContainer.getTypeHistogram() self.typeHistogramPlot.updateTypeHistogram(histogramData) def updateSizeHistogram(self): if self.particleContainer.getNumberOfParticles() > 0: listOfSizeHistograms = [] if self.resultCheckBoxes.showAllCheckBox.isChecked(): totalHist = analysisplots.SizeHistogramData('total', self.particleContainer.getSizesOfAllParticles()) listOfSizeHistograms.append(totalHist) for polymType in self.resultCheckBoxes.getSelectedPolymers(): polymHist = analysisplots.SizeHistogramData(polymType, self.particleContainer.getSizesOfParticleType(polymType)) listOfSizeHistograms.append(polymHist) self.sizeHistogramPlot.drawHistograms(listOfSizeHistograms) def updateSpecPlot(self): hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex) self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, hqi) if self.refSelector.isEnabled() and self.refSelector.currentText() != '': refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText()) ref = self.dbWin.activeDatabase.spectra[refID] self.specPlot.updateReferenceSpectrum(ref[:, 0], ref[:, 1]) def updateContourColors(self): """ Updates the contour colors in the sampleview :return: """ contours = self.viewparent.contourItems alpha = (64 if self.transpAct.isChecked() else 255) selectedPolymers = self.resultCheckBoxes.getSelectedPolymers() for particleIndex, contour in enumerate(contours): assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex) hidden = self.noOverlayAct.isChecked() or (not self.fullOverlayAct.isChecked() and assignment not in selectedPolymers) color = getColorFromNameWithSeed(assignment, self.dataset.colorSeed) color = QtGui.QColor(color[0], color[1], color[2], alpha=alpha) contour.setHidden(hidden) contour.setColor(color) contour.update() def updateLegend(self): """ Updates the color legend in the sampleview :return: """ if not self.noOverlayAct.isChecked(): legendItems = [] selectedPolymers = self.resultCheckBoxes.getSelectedPolymers() for polymer in self.particleContainer.getUniquePolymers(): if self.fullOverlayAct.isChecked() or polymer in selectedPolymers: color = getColorFromNameWithSeed(polymer, self.dataset.colorSeed) color = QtGui.QColor(color[0], color[1], color[2], 255) legendItems.append((polymer, color)) self.viewparent.updateLegend(legendItems) def getSelectedTypeHistogram(self): """ returns the typeHistogram, filtered by the selection of the result CheckBoxes :return: """ totalHist = self.particleContainer.getTypeHistogram() histogramData = {} selectedPolymers = self.resultCheckBoxes.getSelectedPolymers() for polymType in totalHist: if polymType in selectedPolymers: histogramData[polymType] = totalHist[polymType] return histogramData @QtCore.pyqtSlot() def updateFromNavigationToolbar(self): """ Retrieves values from the navigation toolbar and updates the SpecPlot accordingly :return: """ self.currentParticleIndex = self.navigationToolbar.currentParticleIndex self.currentSpectrumIndex = self.navigationToolbar.currentSpectrumIndex self.updateSpecPlot() @QtCore.pyqtSlot() def jumpToIndicatedSpectrum(self): """ Make the sampleView jump to the indicated spectrum number and upates the SpecPlot accordingly :return: """ self.currentSpectrumIndex = self.navigationToolbar.specNumberSelector.value()-1 self.currentParticleIndex = self.particleContainer.getParticleIndexContainingSpecIndex(self.currentSpectrumIndex) self.viewparent.centerOnRamanIndex(self.currentSpectrumIndex) self.viewparent.highLightContour(self.currentParticleIndex) self.updateSpecPlot() def darkenBackground(self): """ Darkens the background of the sampleview to make the overlay easier visible :return: """ self.viewparent.darkenPixmap = self.darkenAct.isChecked() if self.darkenAct.isChecked(): self.viewparent.scene().setBackgroundBrush(QtGui.QColor(5, 5, 5)) self.viewparent.item.setOpacity(0.2) else: self.viewparent.scene().setBackgroundBrush(QtCore.Qt.darkGray) self.viewparent.item.setOpacity(1) def updateColorSeed(self): """ Updates the color Seed of the dataset, which is used to get randomized colors :return: """ text, ok = QtWidgets.QInputDialog.getText(self, 'Color Seed', 'Enter New Seed here', text=self.dataset.colorSeed) if ok: self.dataset.colorSeed = text self.updatePlotsAndContours() def showOrHideSpecNumbers(self): """ Shows or hides spectraLabels in the sampleview """ hidden = self.hideLabelAct.isChecked() for scanIndicator in self.viewparent.ramanscanitems: scanIndicator.hidden = hidden scanIndicator.update() def exportToExcel(self): """ Opens the Excel Export Window :return: """ expWin = ExpExcelDialog(self.dataset) expWin.exec() def exportToSQL(self): """ Opens the SQL Export Window :return: """ def launchSQLWin(): sqlexp = SQLExport(self.dataset) sqlexp.exec() if len(self.dataset.resultsUploadedToSQL) > 0: reply = QtWidgets.QMessageBox.question(self, 'Warning!', "The following results were already uploaded:\n\n{}\n\nContinue?".format('\n'.join(self.dataset.resultsUploadedToSQL)), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: launchSQLWin() else: launchSQLWin() def closeEvent(self, event): for window in [self.importWindow, self.dbWin, self.findcoloredParticlesWindow]: if window is not None: window.close() self.viewparent.imparent.particelAnalysisAct.setChecked(False) event.accept()