Esempio n. 1
0
    def windowArrangement(self):

        verticalBoxWithButtons = QVBoxLayout()

        buttonSplitter = QSplitter(Qt.Vertical)
        buttonSplitter.setFrameShape(QFrame.StyledPanel)
        buttonSplitter.addWidget(self.startButton)
        buttonSplitter.addWidget(self.endButton)
        buttonSplitter.addWidget(self.resetButton)
        buttonSplitter.addWidget(self.clientButton)
        buttonSplitter.addWidget(self.serwerButton)
        verticalBoxWithButtons.addWidget(buttonSplitter)

        verticalBoxWithScene = QVBoxLayout()
        verticalBoxWithScene.addWidget(self.view, alignment= Qt.AlignCenter)
        verticalBoxWithScene.addWidget(self.inputLineEdit)

        horizontalBoxWithAll = QHBoxLayout()
        horizontalBoxWithAll.addLayout(verticalBoxWithScene)
        horizontalBoxWithAll.addLayout(verticalBoxWithButtons)

        self.setLayout(horizontalBoxWithAll)
class AnalyzeTab(QWidget):

    analyzeDone = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject',
                             'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject',
                             'PyQt_PyObject')

    def __init__(self):
        super().__init__()

        # Layouts
        self.mainLayout = QHBoxLayout()
        self.lVbox = QVBoxLayout()
        self.lHbox = QHBoxLayout()
        self.lHbox_top = QHBoxLayout()
        self.rVbox = QVBoxLayout()
        self.rHbox = QHBoxLayout()
        self.rVbox2 = QVBoxLayout()
        self.stack = QStackedWidget()
        self.stack_Vbox = QVBoxLayout()
        self.stack_Hbox1 = QHBoxLayout()
        self.stack_Hbox2 = QHBoxLayout()
        self.hSplit = QSplitter(Qt.Horizontal)
        self.hSplit.setFrameShape(QFrame.StyledPanel)
        self.vSplit = QSplitter(Qt.Vertical)
        self.vSplit.setFrameShape(QFrame.StyledPanel)
        self.mainLayout.addLayout(self.lVbox, 1)
        self.mainLayout.addLayout(self.rVbox, 3)

        # Setup file browser
        self.fileModel = QFileSystemModel()
        self.fileModel.setNameFilters(['*.wav'])
        self.fileModel.setRootPath(QDir.currentPath())
        self.fileTree = QTreeView()
        self.fileTree.setModel(self.fileModel)
        self.fileTree.setRootIndex(self.fileModel.index(r'./'))
        self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection)
        self.fileTree.setColumnHidden(2, True)
        self.fileTree.setColumnHidden(1, True)
        self.rootDirEdit = QLineEdit(os.path.dirname(__file__))
        self.rootDirEdit.returnPressed.connect(self.on_edit_root)
        self.browseBtn = QPushButton('Browse')
        self.browseBtn.clicked.connect(self.on_browse)
        self.lHbox_top.addWidget(self.rootDirEdit, 3)
        self.lHbox_top.addWidget(self.browseBtn, 1)

        # Setup Canvas
        self.canvas = PlotCanvas(self)

        self.analyzeDone.connect(self.canvas.plot)
        self._analyze = lambda _: self.analyze(self.fileTree.selectedIndexes())
        self.analyzeBtn = QPushButton('Analyze')
        self.analyzeBtn.clicked.connect(self._analyze)

        ## BATCH ANALYSIS CONTROLS ##

        self.batchAnalyzeChk = QCheckBox('Batch Analysis')
        self.dataTable = QTableWidget()
        self.batchCtrlBox = QGroupBox("Batch Analysis")
        self.batchCtrlBox.setLayout(self.stack_Vbox)

        # Analysis Mode
        self.modeGroup = QButtonGroup()
        self.modeBox = QGroupBox('Analysis Mode')
        self.modeBox.setLayout(self.stack_Hbox1)
        self.stack_Vbox.addWidget(self.modeBox)
        self.wavAnalysisChk = QCheckBox('Wav analysis')
        self.wavAnalysisChk.setChecked(True)
        self.calibrationLocationBox = QComboBox()
        self.calibrationLocationBox.addItems([str(n) for n in range(1, 11)])
        self.calibrationCurveChk = QCheckBox('Calibration Curve')
        self.calibrationCurveChk.toggled.connect(
            lambda state: self.calibrationLocationBox.setEnabled(state))
        self.calibrationCurveChk.setChecked(False)
        self.stack_Hbox1.addWidget(self.wavAnalysisChk, 3)
        self.stack_Hbox1.addWidget(self.calibrationCurveChk, 3)
        self.stack_Hbox1.addWidget(QLabel('Location: '), 1)
        self.stack_Hbox1.addWidget(self.calibrationLocationBox, 1)
        self.stack_Vbox.addLayout(self.stack_Hbox1)
        self.modeGroup.addButton(self.wavAnalysisChk)
        self.modeGroup.addButton(self.calibrationCurveChk)
        self.modeGroup.setExclusive(True)

        # Outputs
        self.outputCtrlBox = QGroupBox('Outputs')
        self.outputCtrlBox.setLayout(self.stack_Hbox2)
        self.stack_Vbox.addWidget(self.outputCtrlBox)
        self.toCSVchk = QCheckBox('.csv')
        self.toJSONchk = QCheckBox('.json')
        self.toCSVchk.stateChanged.connect(lambda _: self.update_settings(
            'output', 'toCSV', self.toCSVchk.isChecked()))
        self.toJSONchk.stateChanged.connect(lambda _: self.update_settings(
            'output', 'toJSON', self.toJSONchk.isChecked()))

        self.stack_Hbox2.addWidget(self.toCSVchk)
        self.stack_Hbox2.addWidget(self.toJSONchk)
        self.stack_Vbox.addLayout(self.stack_Hbox2)

        self.stack.addWidget(self.dataTable)
        self.stack.addWidget(self.batchCtrlBox)
        self.stack.setCurrentWidget(self.dataTable)
        self.stack.show()
        self.batchAnalyzeChk.stateChanged.connect(self.toggle_stack)
        self.batchAnalyzeChk.setChecked(False)
        self.stack_Vbox.addStretch()

        ## PROCESSING CONTROLS ##
        self.processControls = QGroupBox('Signal Processing')
        self.tOffsetSlider = QSlider(Qt.Horizontal, )
        self.tOffsetSlider.setMinimum(1)
        self.tOffsetSlider.setMaximum(100)
        self.tOffsetSlider.setValue(100)
        self.tOffsetSlider.setTickPosition(QSlider.TicksBelow)
        self.tOffsetSlider.setTickInterval(10)
        self.tOffsetSlider.valueChanged.connect(
            lambda val: self.update_settings('processing', 'tChop', val))
        self.tOffsetLayout = QHBoxLayout()
        self.tOffsetSlider_Box = QGroupBox(
            f'Chop Signal - {self.tOffsetSlider.value()}%')
        self.tOffsetSlider.valueChanged.connect(
            lambda val: self.tOffsetSlider_Box.setTitle(f'Chop Signal - {val}%'
                                                        ))
        self.tOffsetSlider_Box.setLayout(self.tOffsetLayout)
        self.tOffsetLayout.addWidget(self.tOffsetSlider)

        self.nFFTSlider = QSlider(Qt.Horizontal, )
        self.nFFTSlider.setMinimum(1)
        self.nFFTSlider.setMaximum(16)
        self.nFFTSlider.setValue(1)
        self.nFFTSlider.setTickPosition(QSlider.TicksBelow)
        self.nFFTSlider.setTickInterval(2)
        self.nFFTSlider.valueChanged.connect(
            lambda val: self.update_settings('processing', 'detail', val))
        self.nFFTLayout = QHBoxLayout()
        self.nFFTSlider.valueChanged.connect(
            lambda val: self.nFFTSlider_Box.setTitle(f'FFT Size - {val*65536}'
                                                     ))
        self.nFFTSlider_Box = QGroupBox(
            f'FFT Size - {self.nFFTSlider.value()*65536}')
        self.nFFTSlider_Box.setLayout(self.nFFTLayout)
        self.nFFTLayout.addWidget(self.nFFTSlider)

        self.rVbox2.addWidget(self.tOffsetSlider_Box)
        self.rVbox2.addWidget(self.nFFTSlider_Box)
        self.processControls.setLayout(self.rVbox2)

        self.lVbox.addLayout(self.lHbox_top, 1)
        self.lVbox.addWidget(self.fileTree, 7)
        self.lVbox.addLayout(self.lHbox, 1)
        self.lHbox.addWidget(self.analyzeBtn, 2)
        self.lHbox.addWidget(self.batchAnalyzeChk, 1)
        self.vSplit.addWidget(self.canvas)
        self.vSplit.addWidget(self.hSplit)
        self.rVbox.addWidget(self.vSplit)
        self.hSplit.addWidget(self.stack)
        self.hSplit.addWidget(self.processControls)

        self.settings = {
            'processing': {
                'tChop': self.tOffsetSlider.value(),
                'detail': self.nFFTSlider.value()
            },
            'output': {
                'toCSV': self.toCSVchk.isChecked(),
                'toJSON': self.toJSONchk.isChecked()
            }
        }
        self.setLayout(self.mainLayout)

    def on_browse(self):
        # Browse to file tree root directory
        options = QFileDialog.Options()
        path = QFileDialog.getExistingDirectory(
            self, caption="Choose root directory", options=options)
        self.rootDirEdit.setText(path)
        self.fileTree.setRootIndex(self.fileModel.index(path))

    def on_edit_root(self):
        # Update the file tree root directory
        self.fileTree.setRootIndex(
            self.fileModel.index(self.rootDirEdit.text()))

    def update_settings(self, category, setting, value):
        # Update settings and reprocess FFT if in single analysis mode
        self.settings[category][setting] = value

        if category == 'processing' and self.fileTree.selectedIndexes():
            self.analyze(self.fileTree.selectedIndexes())

    def toggle_stack(self, state):
        if state == 2:
            self.stack.setCurrentWidget(self.batchCtrlBox)
            self.fileTree.setSelectionMode(QAbstractItemView.MultiSelection)
        else:
            self.stack.setCurrentWidget(self.dataTable)
            self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection)

    def analyze(self, filePaths):
        if self.batchAnalyzeChk.isChecked():
            if self.wavAnalysisChk.isChecked():
                self.batch_analyze_wav(
                    [self.fileModel.filePath(path) for path in filePaths[::4]])
            if self.calibrationCurveChk.isChecked():
                self.generate_calibration_curve(
                    [self.fileModel.filePath(path) for path in filePaths[::4]])

        else:
            if os.path.isdir(self.fileModel.filePath(
                    filePaths[0])) or len(filePaths) > 4:
                QMessageBox.information(
                    self, 'Error',
                    'Please select only 1 file for single analysis.')
                return
            self.single_analyze_wav(self.fileModel.filePath(filePaths[0]))

    def single_analyze_wav(self, filePath):
        """
        Do an FFT and find peaks on a single wav file

        :param filePath: file path to .wav file
        """

        tChopped, vChopped, fVals,\
        powerFFT, peakFreqs, peakAmps = Utils.AnalyzeFFT(filePath, tChop=self.settings['processing']['tChop'],
                                                                   detail=self.settings['processing']['detail'])

        self.analyzeDone.emit(tChopped, vChopped, fVals, powerFFT, peakFreqs,
                              peakAmps, filePath)
        self.update_table(peakFreqs, peakAmps)

    def batch_analyze_wav(self, filePaths):
        """
        Perform a batch analysis of many .wav files. Outputs FFTs and peaks in .csv or .json format

        :param filePaths: A list of folders containing the .wav files to be analyzed
        """

        toCSV = self.settings['output']['toCSV']
        toJSON = self.settings['output']['toJSON']

        start = time.time()

        fileTotal = 0
        for path in filePaths:
            if os.path.isdir(path):
                blockName = os.path.basename(path)
                print(f'Block: {blockName}')

                files = [
                    os.path.join(path, file) for file in os.listdir(path)
                    if '.wav' in file
                ]
                fileTotal += len(files)

                if toCSV:
                    if not os.path.exists(os.path.join(path,
                                                       'fft_results_csv')):
                        os.makedirs(os.path.join(path, 'fft_results_csv'))
                    resultFilePath = os.path.join(path, 'fft_results_csv')

                    print('Processing FFTs...')
                    with multiprocessing.Pool(processes=4) as pool:
                        results = pool.starmap(
                            Utils.AnalyzeFFT,
                            zip(files, itertools.repeat(True),
                                itertools.repeat(True)))
                    results = [
                        result for result in results if result is not None
                    ]

                    peaks = [result[0] for result in results]
                    ffts = [result[1] for result in results]

                    print('Writing to .csv...')
                    resultFileName = os.path.join(resultFilePath,
                                                  f'{blockName}_Peaks.csv')
                    peakFrames = pd.concat(peaks)
                    peakFrames.to_csv(resultFileName, index=False, header=True)
                    with concurrent.futures.ThreadPoolExecutor(
                            max_workers=16) as executor:
                        executor.map(self.multi_csv_write, ffts)

                if toJSON:
                    if not os.path.exists(
                            os.path.join(path, 'fft_results_json')):
                        os.makedirs(os.path.join(path, 'fft_results_json'))
                    print(os.path.join(path, 'fft_results_json'))

                    print('Processing FFTs...')
                    with multiprocessing.Pool(processes=4) as pool:
                        results = pool.starmap(
                            Utils.AnalyzeFFT,
                            zip(files, itertools.repeat(True),
                                itertools.repeat(False),
                                itertools.repeat(True)))
                        results = [
                            result for result in results if result is not None
                        ]

                    print('Writing to .json...')
                    with concurrent.futures.ThreadPoolExecutor(
                            max_workers=16) as executor:
                        executor.map(self.multi_json_write, results)

        end = time.time()
        print(
            f'**Done!** {len(filePaths)} blocks with {fileTotal} files took {round(end-start, 1)}s'
        )

    def generate_calibration_curve(self, filePaths):
        """
        Attempt to fit an exponential function to a set of data points (x: Peak Frequency, y: Compressive strength)
        provided in JSON format.

        ex:{
              "shape": "2-Hole",
              "testData": {
                "location": "1",
                "strength": 3.092453552,
                "peaks": [
                  {
                    "frequency": 1134.5561082797967,
                    "magnitude": 0.349102384777402
                  }]
              },
              "waveData": [...],
              "freqData": [...]
        }

        Plot the curve, data points and give the function if successful.

        ** NOTE ** This function is still experimental and a bit buggy. Sometimes the scipy.optimize curve_fit won't
        converge with the initial guess given for the coeffecients. You're probably better off writing your own code.

        :param filePaths: A list of folders containing .jsons
        """
        # Strike Location
        location = self.calibrationLocationBox.currentText()

        # Function to fit to the data
        exp_f = lambda x, a, b, c: a * np.exp(b * x) + c

        # Threaded method for opening all the .jsons and fitting
        calibCurve = ThreadedCalibrationCurve(filePaths, location, exp_f)
        progressDialog = QProgressDialog(
            f'Gettings samples for location: {location}', None, 0,
            len(filePaths), self)
        progressDialog.setModal(True)
        calibCurve.blocksSearched.connect(progressDialog.setValue)
        try:
            peakFreqs, strengths, popt, pcov, fitX = calibCurve.run()
        except Exception as e:
            QMessageBox.information(self, 'Error', e)
            return

        # Calculate R Squared
        residuals = strengths - exp_f(peakFreqs, *popt)
        ss_res = np.sum(residuals**2)
        ss_tot = np.sum((strengths - np.mean(strengths))**2)
        r_squared = 1 - (ss_res / ss_tot)

        # Plot Results
        fig = Figure()
        plt.scatter(peakFreqs, strengths)
        plt.plot(fitX, exp_f(fitX, *popt), '-k')
        ax = plt.gca()
        plt.text(
            0.05,
            0.9,
            f'y = {round(popt[0],3)}*exp({round(popt[1], 5)}x) + {round(popt[2], 3)}\n',
            ha='left',
            va='center',
            transform=ax.transAxes)
        plt.text(0.05,
                 0.85,
                 f'R^2 = {round(r_squared,3)}',
                 ha='left',
                 va='center',
                 transform=ax.transAxes)

        plt.title(f'Calibration Curve, Location: {location}')
        plt.xlabel('Frequency (Hz)')
        plt.ylabel('Compressive Strength (MPa)')

        plt.show()

    def multi_csv_write(self, frameTuple):
        frame = frameTuple[1]
        wavPath = frameTuple[0]

        resultFileDir = os.path.join(os.path.dirname(wavPath),
                                     'fft_results_csv')
        resultFileName = os.path.basename(wavPath) + '_fft.csv'
        resultFilePath = os.path.join(resultFileDir, resultFileName)

        frame.to_csv(resultFilePath, index=False, header=True)

    def multi_json_write(self, results):
        data = results[0]
        wavPath = results[1]

        jsonFileDir = os.path.join(os.path.dirname(wavPath),
                                   'fft_results_json')
        resultFileName = os.path.basename(wavPath) + '_fft.json'
        resultFilePath = os.path.join(jsonFileDir, resultFileName)

        # blockName = os.path.basename(os.path.dirname(wavPath))
        # blockDir = os.path.join(jsonFileDir, blockName)
        # if not os.path.exists(blockDir):
        #     os.makedirs(blockDir)
        # print(resultFilePath)
        with open(resultFilePath, 'w') as f:
            json.dump(data, f, indent=2)

    def update_table(self, peakFreqs, peakAmps):
        """

        :param peakFreqs:
        :param peakAmps:
        :return:
        """
        self.dataTable.setRowCount(2)
        self.dataTable.setColumnCount(len(peakFreqs) + 1)

        self.dataTable.setItem(0, 0, QTableWidgetItem("Frequencies: "))
        self.dataTable.setItem(1, 0, QTableWidgetItem("Powers: "))

        for col, freq in enumerate(peakFreqs, start=1):
            self.dataTable.setItem(0, col, QTableWidgetItem(str(round(freq))))
        for col, power in enumerate(peakAmps, start=1):
            item = QTableWidgetItem(str(round(power, 3)))
            if power > 0.7:
                item.setBackground(QColor(239, 81, 28))
            elif power >= 0.4:
                item.setBackground(QColor(232, 225, 34))
            elif power < 0.4:
                item.setBackground(QColor(113, 232, 34))
            self.dataTable.setItem(1, col, item)