class CurveFitting(Module): """Module to fit waves to a function.""" # To add new fitting functions, do the following: # 1) Edit GUI # 2) Modify _parameterTableDefaults # 3) Create a fit[Function] method to do the fitting # 4) Call fit[Function] from doFit # Default values for parameter table # Each dict entry is a list of lists. The inner list contains a row of values. _parameterTableDefaults = { 'Polynomial': [ ['p0', 1], ['p1', 1], ['p2', 1], ], 'Sinusoid': [ ['p0', 1], ['p1', 1], ['p2', 1], ['p3', 1], ], 'Power Law': [ ['y0', 0], ['a', 1], ['k', 1], ], 'Exponential': [ ['y0', 0], ['A', 1], ['b', 1], ], 'Logarithm': [ ['y0', 0], ['a', 1], ['base', 10], ], 'Gaussian': [ ['amp', 1], ['mean', 0], ['width', 1], ], 'Lorentzian': [ ['amp', 1], ['mean', 0], ['hwhm', 1], ], } def __init__(self): Module.__init__(self) def buildWidget(self): self._widget = QWidget() self._ui = Ui_CurveFitting() self._ui.setupUi(self._widget) self.setModels() self.setupSpinBoxes() self.setupParameterTableData() # Connect button signals self._ui.doFitButton.clicked.connect(self.doFit) self._ui.closeButton.clicked.connect(self.closeWindow) self._ui.function.currentIndexChanged[str].connect(self.changeFunction) self._ui.function.currentIndexChanged[str].connect(self.connectSlotsOnFunctionChange) self._ui.initialValuesWave.activated[str].connect(self.changeInitialValuesWave) self._ui.useInitialValuesWave.toggled[bool].connect(self.changeInitialValuesWaveFromCheckbox) self._ui.useWaveForInterpolation.toggled[bool].connect(self.catchInterpolationWaveGroupBoxCheck) self._ui.useDomainForInterpolation.toggled[bool].connect(self.catchInterpolationDomainGroupBoxCheck) self.connectSlotsOnFunctionChange('') def setModels(self): # Set up model and view self._allWavesListModel = self._app.model('appWaves') self._ui.xWave.setModel(self._allWavesListModel) self._ui.yWave.setModel(self._allWavesListModel) self._ui.weightWave.setModel(self._allWavesListModel) self._ui.initialValuesWave.setModel(self._allWavesListModel) self._ui.interpolationWave.setModel(self._allWavesListModel) def setupSpinBoxes(self): self._ui.dataRangeStart.addWaveView(self._ui.xWave) self._ui.dataRangeEnd.addWaveView(self._ui.xWave) self._ui.dataRangeStart.addWaveView(self._ui.yWave) self._ui.dataRangeEnd.addWaveView(self._ui.yWave) self._ui.interpolationWaveRangeStart.addWaveView(self._ui.interpolationWave) self._ui.interpolationWaveRangeEnd.addWaveView(self._ui.interpolationWave) def setupParameterTableData(self): self._parameterTableData = {} self._currentFunction = None self.changeFunction('') def closeWindow(self): self._widget.parent().close() def connectSlotsOnFunctionChange(self, newFunctionName): """ Disconnect slots dependent on which function is chosen. If polynomial function is chosen, connect slot to update parameter table on degree change. """ # Disconnect slots try: self._ui.polynomialDegree.valueChanged[int].disconnect(self.changePolynomialDegree) except: pass # Connect polynomial degree change if Util.getWidgetValue(self._ui.function) == 'Polynomial': self._ui.polynomialDegree.valueChanged[int].connect(self.changePolynomialDegree) def catchInterpolationWaveGroupBoxCheck(self, checked): # Set the opposite check for the domain group box Util.setWidgetValue(self._ui.useDomainForInterpolation, not checked) def catchInterpolationDomainGroupBoxCheck(self, checked): # Set the opposite check for the wave group box Util.setWidgetValue(self._ui.useWaveForInterpolation, not checked) def saveParameterTable(self): """ Save the parameters for the current function to the object. """ if self._currentFunction: self._parameterTableData[self._currentFunction] = self.getCurrentParameterTable() def changeFunction(self, newFunctionName): # Save parameters for old function self.saveParameterTable() #if self._currentFunction: # self._parameterTableData[self._currentFunction] = self.getCurrentParameterTable() # Now update _currentFunction to the function that is currently selected. # If this method was called because the user selected a different function, # then this will be modified. If it was called because the fit curve button # was pressed, then its value will not be changed. self._currentFunction = Util.getWidgetValue(self._ui.function) # Enter in parameters for new function # If there are previously user-entered values, then use them # else, if a wave is selected, then use that # else, use the initial values # Either way, if there are blank entries, then use initial values for them # Clear the table, but leave all the column headers for rowIndex in range(self._ui.parameterTable.rowCount()): self._ui.parameterTable.removeRow(0) parameters = [] # If there is saved data, use it if self._currentFunction in self._parameterTableData: parameters = self._parameterTableData[self._currentFunction] # If there aren't enough rows for all the parameters, extend with # initial values. This will also occur if no parameters had been saved. savedParametersLength = len(parameters) defaultParameters = self._parameterTableDefaults[self._currentFunction] if savedParametersLength < len(defaultParameters): parameters.extend(defaultParameters[len(parameters):]) # Use wave if requested by the user if Util.getWidgetValue(self._ui.useInitialValuesWave): # Convert from QString to str waveName = str(Util.getWidgetValue(self._ui.initialValuesWave)) if self._app.waves().wave(waveName) is None: # waveName is not a name of a wave pass else: waveData = self._app.waves().wave(waveName).data() for i in range(savedParametersLength, len(defaultParameters)): parameters[i][1] = waveData[i] self.writeParametersToTable(parameters) def writeParametersToTable(self, parameters, startRow=0): # Determine how many rows the table should have numRows = startRow + len(parameters) self._ui.parameterTable.setRowCount(numRows) # Now actually write to the table for rowIndex, row in enumerate(parameters, startRow): for colIndex, value in enumerate(row): item = QTableWidgetItem(str(value)) if colIndex == 0: # parameter name, do not want it editable item.setFlags(Qt.ItemIsEnabled) self._ui.parameterTable.setItem(rowIndex, colIndex, item) def changePolynomialDegree(self, newDegree): # If decreasing the degree, just remove the last entries # If increasing the degree, # If a wave is selected, then use that for the new values # else, use the initial values desiredNumRows = newDegree + 1 currentNumRows = self._ui.parameterTable.rowCount() if desiredNumRows == currentNumRows: # Nothing to do return # Set defaults rows = [] for d in range(desiredNumRows): rows.append(['p' + str(d), 1]) self._parameterTableDefaults['Polynomial'] = rows # Update table self._ui.parameterTable.setRowCount(desiredNumRows) if desiredNumRows < currentNumRows: # We are done, because no rows need to be edited return # Degree is being increased parameters = self._parameterTableDefaults['Polynomial'][currentNumRows:desiredNumRows] if Util.getWidgetValue(self._ui.useInitialValuesWave): # Convert from QString to str waveName = str(Util.getWidgetValue(self._ui.initialValuesWave)) if self._app.waves().wave(waveName) is None: # waveName is not a name of a wave pass else: waveData = self._app.waves().wave(waveName).data(currentNumRows, desiredNumRows) for index, value in enumerate(waveData): parameters[index][1] = value self.writeParametersToTable(parameters, currentNumRows) def changeInitialValuesWaveFromCheckbox(self, checked): """ If the useInitialValuesWave checkbox is checked, then call changeInitialValuesWave. """ if checked: self.changeInitialValuesWave(str(Util.getWidgetValue(self._ui.initialValuesWave))) def changeInitialValuesWave(self, waveName): # Use the wave for as many parameters as possible # if the wave is too long, then just use the first n values # if the wave is too short, then leave the current value in place # if there is no current value, then use the initial values if Util.getWidgetValue(self._ui.useInitialValuesWave): # Get the current values, with any undefined values using the initial values parameters = self.currentParametersBackedByDefaults() # Now get the wave values parameters = self.updateParametersListWithWave(parameters, waveName) # Set the table to the parameters self.writeParametersToTable(parameters) def updateParametersListWithWave(self, parameters, waveName): """ Given a list of parameter table rows, and the name of a wave, this will update the parameter values with the entries in the wave. """ waveName = str(waveName) if self._app.waves().wave(waveName) is None: # waveName is not a name of a wave return parameters waveData = self._app.waves().wave(waveName).data(0, len(parameters)) for i in range(len(waveData)): parameters[i][1] = waveData[i] return parameters def currentParametersBackedByDefaults(self): # Start with initial values parameters = self._parameterTableDefaults[self._currentFunction] # Then get the current values and update parameters with it currentParameters = self.getCurrentParameterTable() for rowIndex, row in enumerate(currentParameters): parameters[rowIndex] = row return parameters def getCurrentParameterTable(self): """ Save data to a 2-d array mimicking the table. """ # FIXME only works with text right now. Need to add in support for check boxes # Maybe do this by creating a QTableWidgetItem option in Util.getWidgetValue # and using QTableWidget.cellWidget to get the indiv. cells table = [] row = [] for rowIndex in range(self._ui.parameterTable.rowCount()): for colIndex in range(self._ui.parameterTable.columnCount()): try: row.append(str(self._ui.parameterTable.item(rowIndex, colIndex).text())) except AttributeError: row.append('') table.append(row) row = [] return table def parameterColumnValues(self, functionName, columnNum): """ Return a list of the values of a specific column in the parameter table. """ if functionName not in self._parameterTableData: return None tableData = self._parameterTableData[functionName] values = [str(row[columnNum]) for row in tableData] return values def parameterNames(self, functionName): """ Return a list of the names of the parameters for the given function. """ return self.parameterColumnValues(functionName, 0) def parameterInitialValues(self, functionName): """ Return a list of the initial values of the parameters (NOT the default values) for the given function. """ values = self.parameterColumnValues(functionName, 1) initialValues = [float(v) if Util.isNumber(v) else 1 for v in values] return initialValues def doFit(self): # save user-defined parameters self.saveParameterTable() # Get all waves that are selected before doing anything else # If any waves are created, as they are in the output tab section, # then the wave combo boxes are refreshed, and the previous selection # is lost xWaveName = Util.getWidgetValue(self._ui.xWave) yWaveName = Util.getWidgetValue(self._ui.yWave) weightWaveName = Util.getWidgetValue(self._ui.weightWave) interpolationDomainWaveName = Util.getWidgetValue(self._ui.interpolationWave) # Get data tab dataRangeStart = Util.getWidgetValue(self._ui.dataRangeStart) dataRangeEnd = Util.getWidgetValue(self._ui.dataRangeEnd) xWave = self._app.waves().wave(xWaveName) yWave = self._app.waves().wave(yWaveName) xLength = xWave.length() yLength = yWave.length() # Verify data range limits are valid if dataRangeStart > xLength or dataRangeStart > yLength: dataRangeStart = 0 if dataRangeEnd > xLength or dataRangeEnd > yLength: dataRangeEnd = min(xLength, yLength) - 1 xData = xWave.data(dataRangeStart, dataRangeEnd + 1) yData = yWave.data(dataRangeStart, dataRangeEnd + 1) # Get weights, if required by user if Util.getWidgetValue(self._ui.useWeights): weightWave = self._app.waves().wave(weightWaveName) weightLength = weightWave.length() weightData = weightWave.data(dataRangeStart, dataRangeEnd + 1) # If weighting inversely, invert the weights if Util.getWidgetValue(self._ui.weightIndirectly): weightData = [1./w if w != 0 else 0 for w in weightData] if len(weightData) != len(yData): print "The number of weight points is not the same as the number of y points." return 1 else: weightData = None # Get output tab outputOptions = {} outputWaves = {} outputOptions['createTable'] = Util.getWidgetValue(self._ui.createTable) outputOptions['outputParameters'] = Util.getWidgetValue(self._ui.outputParameters) if outputOptions['outputParameters']: outputOptions['saveLabels'] = Util.getWidgetValue(self._ui.saveLabels) # Create saveLabels wave if outputOptions['saveLabels']: saveLabelsDestination = self._app.waves().findGoodWaveName(Util.getWidgetValue(self._ui.saveLabelsDestination)) outputWaves['saveLabelsWave'] = Wave(saveLabelsDestination, 'String') self._app.waves().addWave(outputWaves['saveLabelsWave']) # Create parameter wave parameterDestination = self._app.waves().findGoodWaveName(Util.getWidgetValue(self._ui.parameterDestination)) outputWaves['parameterWave'] = Wave(parameterDestination, 'Decimal') self._app.waves().addWave(outputWaves['parameterWave']) outputOptions['outputInterpolation'] = Util.getWidgetValue(self._ui.outputInterpolation) if outputOptions['outputInterpolation']: # Create interpolation wave interpolationDestination = self._app.waves().findGoodWaveName(Util.getWidgetValue(self._ui.interpolationDestination)) outputWaves['interpolationDestinationWave'] = Wave(interpolationDestination, 'Decimal') self._app.waves().addWave(outputWaves['interpolationDestinationWave']) if Util.getWidgetValue(self._ui.useWaveForInterpolation): # Using an already-existing wave for the interpolation points. interpolationDomainWave = self._app.waves().wave(interpolationDomainWaveName) interpolationWaveRangeStart = Util.getWidgetValue(self._ui.interpolationWaveRangeStart) interpolationWaveRangeEnd = Util.getWidgetValue(self._ui.interpolationWaveRangeEnd) outputWaves['interpolationDomainWave'] = interpolationDomainWave # Start the wave with as many blanks as necessary in order to get the destination wave # to line up correctly with the domain wave, for easy plotting. outputWaves['interpolationDestinationWave'].extend([''] * interpolationWaveRangeStart) # Verify data range limits are valid interpolationDomainLength = interpolationDomainWave.length() if interpolationWaveRangeStart > interpolationDomainLength: interpolationWaveRangeStart = 0 if interpolationWaveRangeEnd > interpolationDomainLength: interpolationWaveRangeEnd = interpolationDomainLength - 1 outputOptions['interpolationDomainWaveData'] = interpolationDomainWave.data(interpolationWaveRangeStart, interpolationWaveRangeEnd + 1) else: # Creating a new wave based on a domain and number of points. customWaveName = Util.getWidgetValue(self._ui.interpolationCustomWaveName) customLowerLimit = float(Util.getWidgetValue(self._ui.interpolationCustomLowerLimit)) customUpperLimit = float(Util.getWidgetValue(self._ui.interpolationCustomUpperLimit)) customNumPoints = Util.getWidgetValue(self._ui.interpolationCustomNumPoints) outputOptions['interpolationDomainWaveData'] = numpy.linspace(customLowerLimit, customUpperLimit, customNumPoints, endpoint=True) interpolationDomainWaveName = self._app.waves().findGoodWaveName(customWaveName) outputWaves['interpolationDomainWave'] = Wave(interpolationDomainWaveName, 'Decimal', outputOptions['interpolationDomainWaveData']) self._app.waves().addWave(outputWaves['interpolationDomainWave']) outputOptions['saveResiduals'] = Util.getWidgetValue(self._ui.saveResiduals) if outputOptions['saveResiduals']: residualsDestination = self._app.waves().findGoodWaveName(Util.getWidgetValue(self._ui.residualsDestination)) outputWaves['residualsWave'] = Wave(residualsDestination, 'Decimal') self._app.waves().addWave(outputWaves['residualsWave']) # If the fit is not done to all the data in the wave, then we need to add blanks to the beginning # of the residual wave because the residuals will only be calculated for the part of the data that # was actually fit. outputWaves['residualsWave'].extend([''] * dataRangeStart) # Save the x wave, in case it is different from the interpolationDomainWave outputWaves['xWave'] = xWave # Determine the function and call the appropriate method functionName = Util.getWidgetValue(self._ui.function) if functionName == 'Polynomial': self.fitPolynomial(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Sinusoid': self.fitSinusoid(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Power Law': self.fitPowerLaw(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Exponential': self.fitExponential(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Logarithm': self.fitLogarithm(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Gaussian': self.fitGaussian(xData, yData, weightData, outputWaves, outputOptions) elif functionName == 'Lorentzian': self.fitLorentzian(xData, yData, weightData, outputWaves, outputOptions) def fitPolynomial(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): # Get the degree of the polynomial the user wants to use degree = Util.getWidgetValue(self._ui.polynomialDegree) def polynomialFunction(p, x): # If x is a list, then val needs to be a list # If x is a number, then val needs to be a number if isinstance(x, list): val = numpy.array([p[0]] * len(x)) else: val = p[0] # Add x, x^2, x^3, etc entries for d in range(1, degree + 1): val += numpy.multiply(p[d], numpy.power(x, d)) return val parameterNames = self.parameterNames('Polynomial') initialValues = self.parameterInitialValues('Polynomial') if initialValues is None: initialValues = [1] * degree self.fitFunction(polynomialFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Polynomial Fit') def fitSinusoid(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): sinusoidFunction = lambda p, x: p[0] + p[1] * numpy.cos(x / p[2] * 2. * numpy.pi + p[3]) parameterNames = self.parameterNames('Sinusoid') initialValues = self.parameterInitialValues('Sinusoid') if initialValues is None: initialValues = [1, 1, 1, 1] self.fitFunction(sinusoidFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Sinusoid Fit') def fitPowerLaw(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): powerLawFunction = lambda p, x: numpy.add(p[0], numpy.multiply(p[1], numpy.power(x, p[2]))) parameterNames = self.parameterNames('Power Law') initialValues = self.parameterInitialValues('Power Law') if initialValues is None: initialValues = [0, 1, 1] self.fitFunction(powerLawFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Power Law Fit') def fitExponential(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): exponentialFunction = lambda p, x: numpy.add(p[0], numpy.multiply(p[1], numpy.power(numpy.e, numpy.multiply(p[2], x)))) parameterNames = self.parameterNames('Exponential') initialValues = self.parameterInitialValues('Exponential') if initialValues is None: initialValues = [0, 1, 1] self.fitFunction(exponentialFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Exponential Fit') def fitLogarithm(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): # There is no numpy log function where you can specify a custom base, so we'll define one customBaseLog = lambda base, x: numpy.divide(numpy.log(x), numpy.log(base)) logarithmFunction = lambda p, x: numpy.add(p[0], numpy.multiply(p[1], customBaseLog(p[2], x))) parameterNames = self.parameterNames('Logarithm') initialValues = self.parameterInitialValues('Logarithm') if initialValues is None: initialValues = [0, 1, 10] self.fitFunction(logarithmFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Logarithm Fit') def fitGaussian(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): gaussianFunction = lambda p, x: numpy.multiply(p[0], numpy.power(numpy.e, numpy.divide(-1 * numpy.power((numpy.subtract(x, p[1])), 2), 2 * numpy.power(p[2], 2)))) parameterNames = self.parameterNames('Gaussian') initialValues = self.parameterInitialValues('Gaussian') if initialValues is None: initialValues = [1, 0, 1] self.fitFunction(gaussianFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Gaussian Fit') def fitLorentzian(self, xData, yData, weightData=None, outputWaves={}, outputOptions={}): lorentzianFunction = lambda p, x: numpy.divide(numpy.multiply(p[0], p[2]), numpy.add(numpy.power(numpy.subtract(x, p[1]), 2), numpy.power(p[2], 2))) parameterNames = self.parameterNames('Lorentzian') initialValues = self.parameterInitialValues('Lorentzian') if initialValues is None: initialValues = [1, 0, 1] self.fitFunction(lorentzianFunction, parameterNames, initialValues, xData, yData, weightData, outputWaves, outputOptions, 'Lorentzian Fit') def fitFunction(self, function, parameterNames, initialValues, xData, yData, weightData=None, outputWaves={}, outputOptions={}, tableName='Fit'): # Can also include initial guesses for the parameters, as well as sigma's for weighting of the ydata # Need to fail with error message if the leastsq call does not succeed # Do the fit result = self.fitFunctionLeastSquares(function, initialValues, xData, yData, weightData) parameters = result[0] tableWaves = [] # Deal with the parameter-related waves if outputOptions['outputParameters']: # save parameter labels if outputOptions['saveLabels']: tableWaves.append(outputWaves['saveLabelsWave']) outputWaves['saveLabelsWave'].extend(parameterNames) tableWaves.append(outputWaves['parameterWave']) # save parameters to a wave outputWaves['parameterWave'].extend(parameters) # Do the interpolation if outputOptions['outputInterpolation']: domain = outputOptions['interpolationDomainWaveData'] determinedFunction = lambda x: function(parameters, x) for val in domain: outputWaves['interpolationDestinationWave'].push(determinedFunction(val)) tableWaves.append(outputWaves['interpolationDomainWave']) tableWaves.append(outputWaves['interpolationDestinationWave']) # Do the residuals if outputOptions['saveResiduals']: residualsFunc = lambda p, x, y: numpy.subtract(y, function(p, x)) residuals = residualsFunc(parameters, xData, yData) outputWaves['residualsWave'].extend(residuals) tableWaves.append(outputWaves['xWave']) tableWaves.append(outputWaves['residualsWave']) # Create table if outputOptions['createTable']: self.createTable(tableWaves, tableName) def fitFunctionLeastSquares(self, func, guess, xData, yData, weightData=None): """ Do a least squares fit for a generic function. func must have the signature (p, x) where p is a list of parameters and x is a float. guess is the user's guess of the parameters, and must be a list of length len(p). xData and yData are the data to fit. """ if weightData is None: #errorFunc = lambda p, x, y: func(p, x) - y errorFunc = lambda p, x, y: numpy.subtract(func(p, x), y) return scipy.optimize.leastsq(errorFunc, guess[:], args=(xData, yData), full_output=True) else: errorFunc = lambda p, x, y, w: numpy.multiply(w, numpy.subtract(func(p, x), y)) return scipy.optimize.leastsq(errorFunc, guess[:], args=(xData, yData, weightData), full_output=True) #return scipy.optimize.leastsq(errorFunc, guess[:], args=(xData, yData), full_output=True) def createTable(self, waves=[], title='Fit'): if len(waves) == 0: return self._app.createTable(waves, title) def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionCurveFiting") self.menuEntry.setText("Curve Fitting") self.menuEntry.triggered.connect(self.window.show) self.menu = vars(self._app.ui)["menuData"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def reload(self): self.setModels() self.setupSpinBoxes() self.setupParameterTableData() Util.setWidgetValue(self._ui.function, 'Polynomial') Util.setWidgetValue(self._ui.useInitialValuesWave, False)
class ManageWavesWidget(Module): """Module to display the Manage Waves dialog window.""" def __init__(self): Module.__init__(self) def buildWidget(self): """Create the widget and populate it.""" # Create enclosing widget and UI self._widget = QWidget() self._ui = Ui_ManageWavesWidget() self._ui.setupUi(self._widget) self.setModels() # Connect some slots self._ui.copyWaveOriginalWave.activated.connect(self.resetCopyWaveLimits) self._ui.createWaveButton.clicked.connect(self.createWave) self._ui.functionInsertWaveButton.clicked.connect(self.insertWaveIntoFunction) self._ui.modifyWave_selectWave.selectionModel().currentChanged.connect(self.updateModifyWaveUi) self._ui.removeWaveButton.clicked.connect(self.removeWave) self._ui.resetWaveButton.clicked.connect(self.updateModifyWaveUi) self._ui.modifyWaveButton.clicked.connect(self.modifyWave) # Make sure selection list and stack are aligned self._ui.waveDataSelectionList.setCurrentRow(0) self._ui.waveDataStack.setCurrentIndex(0) return self._widget def setModels(self): # Set up model and views self._wavesListModel = self._app.model("appWaves") self._ui.copyWaveOriginalWave.setModel(self._wavesListModel) self._ui.functionInsertWave.setModel(self._wavesListModel) self._ui.modifyWave_selectWave.setModel(self._wavesListModel) #### # Create Wave tab #### def createWave(self): """ Create the wave, using whatever starting point (basic, copy, function, etc) is necessary. """ # Check if the wave is unique in the application if not self._app.waves().goodWaveName(Util.getWidgetValue(self._ui.createWave_waveName)): warningMessage = QMessageBox() warningMessage.setWindowTitle("Error!") warningMessage.setText("The name you chose has already been used. Please enter a new name.") warningMessage.setIcon(QMessageBox.Critical) warningMessage.setStandardButtons(QMessageBox.Ok) warningMessage.setDefaultButton(QMessageBox.Ok) result = warningMessage.exec_() return False wave = Wave( Util.getWidgetValue(self._ui.createWave_waveName), Util.getWidgetValue(self._ui.createWave_dataType) ) # Check how the wave should be initially populated initialWaveDataTab = self._ui.waveDataStack.currentWidget().objectName() if initialWaveDataTab == "basicTab": # Basic wave. Need to determine the type basicWaveType = Util.getWidgetValue(self._ui.basicWaveType) if basicWaveType == "Blank": pass elif basicWaveType == "Index (starting at 0)": basicWaveLength = Util.getWidgetValue(self._ui.basicWaveLength) wave.extend(range(0, basicWaveLength)) elif basicWaveType == "Index (starting at 1)": basicWaveLength = Util.getWidgetValue(self._ui.basicWaveLength) wave.extend(range(1, basicWaveLength + 1)) elif initialWaveDataTab == "copyTab": # Copy the data from another wave originalWave = ( self._ui.copyWaveOriginalWave.model() .index(self._ui.copyWaveOriginalWave.currentIndex(), 0) .internalPointer() ) startingIndex = Util.getWidgetValue(self._ui.copyWaveStartingIndex) endingIndex = Util.getWidgetValue(self._ui.copyWaveEndingIndex) wave.extend(originalWave.data(startingIndex, endingIndex)) elif initialWaveDataTab == "functionTab": waveLength = Util.getWidgetValue(self._ui.functionWaveLength) functionString = Util.getWidgetValue(self._ui.functionEquation) data = self.parseFunction(waveLength, functionString) wave.extend(data) # Add wave to application self._app.waves().addWave(wave) # Reset certain ui fields self._ui.copyWaveOriginalWave.setCurrentIndex(0) self._ui.functionInsertWave.setCurrentIndex(0) def resetCopyWaveLimits(self, null=None): """ Reset the spin boxes for selecting the data limits to the current wave's max values. """ wave = ( self._ui.copyWaveOriginalWave.model() .index(self._ui.copyWaveOriginalWave.currentIndex(), 0) .internalPointer() ) maxIndex = wave.length() self._ui.copyWaveStartingIndex.setMaximum(maxIndex) self._ui.copyWaveEndingIndex.setMaximum(maxIndex) Util.setWidgetValue(self._ui.copyWaveEndingIndex, maxIndex) def parseFunction(self, waveLength, functionString): """ Parse the function string into an actual function and return the data for the wave. Any python math and numpy functions are allowed. Special values: w_name - the wave with name 'name' s_val - a special value, see the list below s_ values: s_index - 0-based row index s_oneindex - 1-based row index """ specialValuesList = Util.uniqueList(re.findall("s_\w*", functionString)) specialValuesString = str.join(", ", specialValuesList) wavesList = Util.uniqueList(re.findall("w_\w*", functionString)) wavesString = str.join(", ", wavesList) specialValuesAndWavesList = Util.uniqueList(re.findall("[s|w]_\w*", functionString)) specialValuesAndWavesString = str.join(", ", specialValuesAndWavesList) # First, let's check if this is a simple function that can be performed completely # with built-ins, in which case we don't need to worry about getting any special # values and waves if len(specialValuesAndWavesList) == 0: return eval(functionString) # Need to create the lambda string first so that we can expand all # the arguments to the lambda before creating the actual anonymous # function. function = eval("lambda " + specialValuesAndWavesString + ": eval('" + str(functionString) + "')") # Determine the length of the smallest wave, so that we don't apply the function past that waveLengths = [] if waveLength > 0: waveLengths.append(waveLength) for waveName in wavesList: waveNameNoPrefix = waveName[2:] waveLengths.append(len(self._app.waves().wave(waveNameNoPrefix).data())) if len(waveLengths) == 0: waveLength = 0 else: waveLength = min(waveLengths) # Define s_ values s_index = range(waveLength) s_oneindex = range(1, waveLength + 1) # Define waves that are used in the function for waveName in wavesList: waveNameNoPrefix = waveName[2:] exec(str(waveName) + " = " + str(self._app.waves().wave(waveNameNoPrefix).data()[:waveLength])) # Apply the function data = eval("map(function, " + specialValuesAndWavesString + ")") return data def insertWaveIntoFunction(self): """ Take the wave from functionInsertWave and insert it into the function definition. """ waveName = ( self._ui.functionInsertWave.model() .index(self._ui.functionInsertWave.currentIndex(), 0) .internalPointer() .name() ) self._ui.functionEquation.insert("w_" + str(waveName)) return True #### # Modify/Remove Wave tab #### def updateModifyWaveUi(self, *args): """ Update the wave options based on the current wave. This slot will be called whenever the selection has changed. """ if self._ui.modifyWave_selectWave.currentIndex(): wave = self._wavesListModel.waveByRow(self._ui.modifyWave_selectWave.currentIndex().row()) if wave: Util.setWidgetValue(self._ui.modifyWave_waveName, wave.name()) Util.setWidgetValue(self._ui.modifyWave_dataType, wave.dataType()) def modifyWave(self): """ Set the selected wave to have the currently-selected options. """ currentIndex = self._ui.modifyWave_selectWave.currentIndex() if currentIndex: wave = self._wavesListModel.waveByRow(self._ui.modifyWave_selectWave.currentIndex().row()) # Make sure the user wants to change the wave's name if wave.name() != Util.getWidgetValue(self._ui.modifyWave_waveName) and not self._app.waves().goodWaveName( Util.getWidgetValue(self._ui.modifyWave_waveName) ): warningMessage = QMessageBox() warningMessage.setWindowTitle("Error!") warningMessage.setText( "You are trying to change the wave name, but the one you have chosen has already been used. Please enter a new name." ) warningMessage.setIcon(QMessageBox.Critical) warningMessage.setStandardButtons(QMessageBox.Ok) warningMessage.setDefaultButton(QMessageBox.Ok) result = warningMessage.exec_() return False # Make sure the user wants to actually change the data type if wave.dataType() != Util.getWidgetValue(self._ui.modifyWave_dataType): warningMessage = QMessageBox() warningMessage.setWindowTitle("Warning!") warningMessage.setText( "If you change the data type, then you may lose data if it cannot be properly converted." ) warningMessage.setInformativeText("Are you sure you want to continue?") warningMessage.setIcon(QMessageBox.Warning) warningMessage.setStandardButtons(QMessageBox.Yes | QMessageBox.No) warningMessage.setDefaultButton(QMessageBox.No) result = warningMessage.exec_() if result != QMessageBox.Yes: return False # All warnings have been accepted, so we can continue with actually modifying the wave wave.setName(Util.getWidgetValue(self._ui.modifyWave_waveName)) wave.setDataType(Util.getWidgetValue(self._ui.modifyWave_dataType)) self._ui.modifyWave_selectWave.setCurrentIndex(currentIndex) return True def removeWave(self): """Remove wave from the list of all waves in the main window.""" wavesToRemove = [] currentIndex = self._ui.modifyWave_selectWave.currentIndex() row = currentIndex.row() if currentIndex: warningMessage = QMessageBox() warningMessage.setWindowTitle("Warning!") warningMessage.setText("You are about to delete a wave.") warningMessage.setInformativeText("Are you sure you want to continue?") warningMessage.setIcon(QMessageBox.Warning) warningMessage.setStandardButtons(QMessageBox.Yes | QMessageBox.No) warningMessage.setDefaultButton(QMessageBox.No) result = warningMessage.exec_() if result != QMessageBox.Yes: return False # Determine the next row to select newRow = row if row >= self._wavesListModel.rowCount() - 1: newRow = row - 1 self._app.waves().removeWave(self._wavesListModel.waveNameByRow(row)) self._ui.modifyWave_selectWave.setCurrentIndex(self._wavesListModel.index(newRow)) self._ui.modifyWave_selectWave.selectionModel().select( self._wavesListModel.index(newRow), QItemSelectionModel.ClearAndSelect ) def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionManageWavesWidget") self.menuEntry.setText("Manage Waves") self.menuEntry.triggered.connect(self.window.show) self.menu = vars(self._app.ui)["menuData"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): # Disconnect some slots self.menuEntry.triggered.disconnect() self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def reload(self): self.setModels()
class ImportCSV(Module): """Module to import data as CSV (or TSV, etc).""" def __init__(self): Module.__init__(self) def buildWidget(self): self._widget = QWidget() self._ui = Ui_ImportCSV() self._ui.setupUi(self._widget) # Connect button signals self._ui.csvFileNameButton.clicked.connect(self.csvFileSelector) self._ui.loadDataButton.clicked.connect(self.loadData) self._ui.importDataButton.clicked.connect(self.importData) self._ui.data.cellChanged.connect(self.handleDataChange) def csvFileSelector(self): """Button-lineedit link""" directory = os.path.dirname(Util.getWidgetValue(self._ui.csvFileName)) if not os.path.isdir(directory): directory = self._app.preferences.getInternal('projectDirectory') csvFile = str(QFileDialog.getOpenFileName(self._app.ui.workspace, "Select Data File", directory, "Comma Separated Values (*.csv);;All Files(*)")) if csvFile != "": return Util.setWidgetValue(self._ui.csvFileName, csvFile) return False def loadData(self): """Load data into the widget for viewing before importing into the application.""" # Block the cellChanged signal, because all the cells are going to be changed and we # are dealing with that in this method. self._ui.data.blockSignals(True) dataTable = self._ui.data self.clearTable() # Get data from file csvFile = Util.getWidgetValue(self._ui.csvFileName) if os.path.isfile(csvFile): rows = [] delimiterText = Util.getWidgetValue(self._ui.delimiterButtonGroup) if delimiterText == "Comma": rows = csv.reader(open(csvFile), dialect="excel", delimiter=",") elif delimiterText == "Tab": rows = csv.reader(open(csvFile), dialect="excel-tab") elif delimiterText == "Other": rows = csv.reader(open(csvFile), dialect="excel", delimiter=Util.getWidgetValue(self._ui.otherDelimiter)) else: rows = csv.reader(open(csvFile), dialect="excel", delimiter=",") for rownum, row in enumerate(rows): # Make sure the cells exist to enter the data into dataTable.insertRow(rownum) if len(row) > dataTable.columnCount(): for i in range(dataTable.columnCount(), len(row)): dataTable.insertColumn(dataTable.columnCount()) # Add this row to the table for colnum, item in enumerate(row): dataTable.setItem(rownum, colnum, QTableWidgetItem(item)) # Hide the QT default column header, since we cannot edit it easily dataTable.horizontalHeader().hide() # Add a row for setting the data type of each wave dataTable.insertRow(0) defaultType = Util.getWidgetValue(self._ui.defaultDataType) for col in range(dataTable.columnCount()): typeBox = QComboBox() typeBox.addItem("Integer") typeBox.addItem("Decimal") typeBox.addItem("String") Util.setWidgetValue(typeBox, defaultType) dataTable.setCellWidget(0, col, typeBox) # Potential wave names waveNamePrefix = Util.getWidgetValue(self._ui.waveNamePrefix) tempWaveNames = [] if Util.getWidgetValue(self._ui.useWaveNamePrefix): tempWaveNames = self._app.waves().findGoodWaveNames(dataTable.columnCount(), waveNamePrefix) else: tempWaveNames = self._app.waves().findGoodWaveNames(dataTable.columnCount()) # Wave names can either be generated or taken from the first row in the file if not Util.getWidgetValue(self._ui.firstRowWaveNames): # Generate headers dataTable.insertRow(1) # PyQt does not have QList support yet, so this is a hack to get around that for col,name in enumerate(tempWaveNames): dataTable.setItem(1, col, QTableWidgetItem(name)) else: # Use the first row of data, but check to see if there is text for each column # and if there is no text for a column, add in a tempWaveName entry for col in range(dataTable.columnCount()): if not dataTable.item(1, col) or str(dataTable.item(1, col).text()).strip() == "": dataTable.setItem(1, col, QTableWidgetItem(tempWaveNames.pop(0))) else: # For the names that came from the file, add the prefix specified by the user if Util.getWidgetValue(self._ui.useWaveNamePrefix): dataTable.item(1, col).setText(str(waveNamePrefix) + dataTable.item(1, col).text()) # Edit the name so that it could be valid (no spaces, etc). But it might # still be a duplicate. self.validateWaveNames() # Adjust the row headers so that they number correctly with the wave names in the first row rowLabels = QStringList("Type") rowLabels.append("Name") for row in range(1, dataTable.rowCount()): rowLabels.append(str(row)) dataTable.setVerticalHeaderLabels(rowLabels) # Verify that all wave names are acceptable for the app's waves object self.verifyGoodWaveNames() self._ui.data.blockSignals(False) # Resize rows and columns dataTable.resizeRowsToContents() dataTable.resizeColumnsToContents() def clearTable(self): dataTable = self._ui.data # Reset table to be empty for i in range(dataTable.rowCount()): dataTable.removeRow(0) for i in range(dataTable.columnCount()): dataTable.removeColumn(0) def validateWaveNames(self): """ Run Wave.validateWaveName on all potential wave names. """ dataTable = self._ui.data for col in range(dataTable.columnCount()): if dataTable.item(1, col): dataTable.setItem(1, col, QTableWidgetItem(Wave.validateWaveName(str(dataTable.item(1, col).text())))) def verifyGoodWaveNames(self): """ Verify that all the wave names provided will be acceptable in the application's waves object. If there are any conflicts, then disable importing and mark the offending names. """ dataTable = self._ui.data allNamesAreGood = True importWaveNames = [] for col in range(dataTable.columnCount()): if dataTable.item(1, col): name = str(dataTable.item(1, col).text()) if name in importWaveNames or not self._app.waves().goodWaveName(name): # Bad name, highlight dataTable.item(1, col).setBackground(QBrush(QColor('red'))) allNamesAreGood = False else: # Good name dataTable.item(1, col).setBackground(QBrush(QColor('lightgreen'))) importWaveNames.append(name) # Disable the import button if allNamesAreGood: self._ui.importDataButton.setEnabled(True) else: self._ui.importDataButton.setEnabled(False) return allNamesAreGood def handleDataChange(self, row, column): """ If data in a cell is changed, then this method will be called. Currently, if a name cell is edited, then we re-verify the wave names. """ if row == 1: self.verifyGoodWaveNames() def importData(self): """Import data into the application as waves.""" dataTable = self._ui.data # Loop through all waves for col in range(dataTable.columnCount()): dataType = Util.getWidgetValue(dataTable.cellWidget(0, col)) wave = Wave(str(dataTable.item(1, col).text()), dataType) for row in range(2, dataTable.rowCount()): if dataTable.item(row, col): wave.push(str(dataTable.item(row, col).text())) else: wave.push('') self._app.waves().addWave(wave) # Close window self.clearTable() self.window.hide() def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionImportCSV") self.menuEntry.setShortcut("Ctrl+I") self.menuEntry.setText("Import CSV Data") self.menuEntry.triggered.connect(self.window.show) # Check if menu already exists if "menuImport" not in vars(self._app.ui).keys(): self._app.ui.menuImport = QMenu(self._app.ui.menuFile) self._app.ui.menuImport.setObjectName("menuImport") self._app.ui.menuImport.setTitle(QApplication.translate("MainWindow", "Import", None, QApplication.UnicodeUTF8)) self._app.ui.menuFile.addAction(self._app.ui.menuImport.menuAction()) self.menu = vars(self._app.ui)["menuImport"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def setDefaults(self): for child in self._widget.children(): print child del child self._ui.setupUi(self._widget)
class ExportData(Module): """Module to export data in any format.""" def __init__(self): Module.__init__(self) def buildWidget(self): self._widget = QWidget() self._ui = Ui_ExportData() self._ui.setupUi(self._widget) self.setModels() # Connect button signals self._ui.fileNameButton.clicked.connect(self.fileSelector) self._ui.addWaveButton.clicked.connect(self.addWave) self._ui.removeWaveButton.clicked.connect(self.removeWave) self._ui.exportDataButton.clicked.connect(self.exportData) def setModels(self): # Set up model and view self._allWavesListModel = self._app.model('appWaves') self._ui.allWavesListView.setModel(self._allWavesListModel) self._fileWavesListModel = WavesListModel([]) self._ui.fileWavesListView.setModel(self._fileWavesListModel) def fileSelector(self): """Button-lineedit link""" directory = os.path.dirname(Util.getWidgetValue(self._ui.fileName)) if not os.path.isdir(directory): directory = self._app.preferences.getInternal('projectDirectory') fileName = str(QFileDialog.getOpenFileName(self._app.ui.workspace, "Select Data File", directory, "All Files(*)")) if fileName != "": return Util.setWidgetValue(self._ui.fileName, fileName) return False def addWave(self): selectedIndexes = self._ui.allWavesListView.selectedIndexes() for index in selectedIndexes: self._fileWavesListModel.appendRow(self._allWavesListModel.waveNameByRow(index.row())) def removeWave(self): selectedIndexes = self._ui.fileWavesListView.selectedIndexes() selectedRows = map(lambda x:x.row(), selectedIndexes) selectedRows.sort() selectedRows.reverse() for row in selectedRows: self._fileWavesListModel.removeRow(row) def exportData(self): fileName = Util.getWidgetValue(self._ui.fileName) if os.path.exists(fileName): if os.path.isfile(fileName): # Ask about overwriting warningMessage = QMessageBox() warningMessage.setWindowTitle("Warning!") warningMessage.setText("The filename you have chosen - " + str(fileName) + " - already exists.") warningMessage.setInformativeText("Do you want to overwrite the file?") warningMessage.setIcon(QMessageBox.Warning) warningMessage.setStandardButtons(QMessageBox.Yes | QMessageBox.No) warningMessage.setDefaultButton(QMessageBox.No) result = warningMessage.exec_() if result != QMessageBox.Yes: return False else: # Do not try to overwrite a directory or link return False # Get waves waveNames = self._ui.fileWavesListView.model().orderedWaveNames() with open(fileName, 'w') as fileHandle: if Util.getWidgetValue(self._ui.outputType) == 'Delimited': delimiterText = Util.getWidgetValue(self._ui.delimiterButtonGroup) if delimiterText == 'Comma': fileWriter = csv.writer(fileHandle, dialect="excel", delimiter=",") elif delimiterText == 'Tab': fileWriter = csv.writer(fileHandle, dialect="excel-tab") elif delimiterText == 'Other': fileWriter = csv.writer(fileHandle, dialect="excel", delimiter=Util.getWidgetValue(self._ui.delimitedOtherDelimiter)) else: fileWriter = csv.writer(fileHandle, dialect="excel", delimiter=",") dataDirection = Util.getWidgetValue(self._ui.dataDirectionButtonGroup) rows = [] for waveName in waveNames: wave = self._app.waves().wave(waveName) row = wave.data() row.insert(0, wave.name()) rows.append(row) if dataDirection == "Rows": fileWriter.writerows(rows) elif dataDirection == "Columns": # Transpose the rows into columns columns = map(lambda *row: ['' if elem is None else elem for elem in row], *rows) fileWriter.writerows(columns) def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionExportData") self.menuEntry.setShortcut("Ctrl+E") self.menuEntry.setText("Export Data") self.menuEntry.triggered.connect(self.window.show) # Check if menu already exists if "menuExport" not in vars(self._app.ui).keys(): self._app.ui.menuExport = QMenu(self._app.ui.menuFile) self._app.ui.menuExport.setObjectName("menuExport") self._app.ui.menuExport.setTitle(QApplication.translate("MainWindow", "Export", None, QApplication.UnicodeUTF8)) self._app.ui.menuFile.addAction(self._app.ui.menuExport.menuAction()) self.menu = vars(self._app.ui)["menuExport"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def reload(self): self.setModels()
class CreateTableDialog(Module): """Module to display the Create Table dialog window.""" def __init__(self): Module.__init__(self) def buildWidget(self): """Create the widget and populate it.""" # Create enclosing widget and UI self._widget = QWidget() self._ui = Ui_CreateTableDialog() self._ui.setupUi(self._widget) self.setModels() # Connect some slots self._ui.createTableButton.clicked.connect(self.createTable) self._ui.closeWindowButton.clicked.connect(self.closeWindow) self._ui.addWaveButton.clicked.connect(self.addWaveToTable) self._ui.removeWaveButton.clicked.connect(self.removeWaveFromTable) def setModels(self): # Set up model and view self._allWavesListModel = self._app.model('appWaves') self._ui.allWavesListView.setModel(self._allWavesListModel) self._tableWavesListModel = WavesListModel([]) self._ui.tableWavesListView.setModel(self._tableWavesListModel) def closeWindow(self): self.resetForm() self._widget.parent().close() def addWaveToTable(self): selectedIndexes = self._ui.allWavesListView.selectedIndexes() for index in selectedIndexes: self._tableWavesListModel.appendRow(self._allWavesListModel.waveNameByRow(index.row())) def removeWaveFromTable(self): selectedIndexes = self._ui.tableWavesListView.selectedIndexes() selectedRows = map(lambda x:x.row(), selectedIndexes) selectedRows.sort() selectedRows.reverse() for row in selectedRows: self._tableWavesListModel.removeRow(row) def createTable(self): """ Create the table. """ tableName = Util.getWidgetValue(self._ui.tableName) waves = self._tableWavesListModel.waves() if len(waves) == 0: warningMessage = QMessageBox() warningMessage.setWindowTitle("Problem!") warningMessage.setText("You must select at least one wave in order to create a table.") warningMessage.setIcon(QMessageBox.Critical) warningMessage.setStandardButtons(QMessageBox.Ok) warningMessage.setDefaultButton(QMessageBox.Ok) result = warningMessage.exec_() return False names = map(Wave.getName, waves) self._app.createTable(waves, tableName) self.closeWindow() return True def resetForm(self): Util.setWidgetValue(self._ui.tableName, "Table") self._allWavesListModel.doReset() self._tableWavesListModel.removeAllWaves() self._tableWavesListModel.doReset() def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionNewTableDialog") self.menuEntry.setShortcut("Ctrl+T") self.menuEntry.setText("New Table") self.menuEntry.triggered.connect(self.window.show) self.menu = vars(self._app.ui)["menuNew"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): # Disconnect some slots self.menuEntry.triggered.disconnect() self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def reload(self): self.setModels() self.resetForm()
class ImportBinary(Module): """Module to import data from a binary file.""" dataTypes = { 'Signed Integer (1)': { 'dtype': 'Integer', 'char': 'b', 'numbytes': 1 }, 'Unsigned Integer (1)': { 'dtype': 'Integer', 'char': 'B', 'numbytes': 1 }, 'Short (2)': { 'dtype': 'Integer', 'char': 'h', 'numbytes': 2 }, 'Unsigned Short (2)': { 'dtype': 'Integer', 'char': 'H', 'numbytes': 2 }, 'Integer (4)': { 'dtype': 'Integer', 'char': 'i', 'numbytes': 4 }, 'Unsigned Integer (4)': { 'dtype': 'Integer', 'char': 'I', 'numbytes': 4 }, 'Long (8)': { 'dtype': 'Integer', 'char': 'l', 'numbytes': 8 }, 'Unsigned Long (8)': { 'dtype': 'Integer', 'char': 'L', 'numbytes': 8 }, 'Float (4)': { 'dtype': 'Decimal', 'char': 'f', 'numbytes': 4 }, 'Double (8)': { 'dtype': 'Decimal', 'char': 'd', 'numbytes': 8 }, } byteOrders = { 'Native': '@', 'Big Endian': '>', 'Little Endian': '<', 'Network': '!', } def __init__(self): Module.__init__(self) def buildWidget(self): self._widget = QWidget() self._ui = Ui_ImportBinary() self._ui.setupUi(self._widget) # Connect button signals self._ui.fileNameButton.clicked.connect(self.fileSelector) self._ui.importDataButton.clicked.connect(self.importData) def fileSelector(self): """Button-lineedit link""" directory = os.path.dirname(Util.getWidgetValue(self._ui.fileName)) if not os.path.isdir(directory): directory = self._app.preferences.getInternal('projectDirectory') fileName = str(QFileDialog.getOpenFileName(self._app.ui.workspace, "Select Data File", directory, "Binary (*.bin *.dat);;All Files(*)")) if fileName != "": return Util.setWidgetValue(self._ui.fileName, fileName) return False def importData(self): """Import data into the application as waves.""" # Check if the proposed wave name is acceptable validatedWaveName = Wave.validateWaveName(Util.getWidgetValue(self._ui.waveName)) if not self._app.waves().goodWaveName(validatedWaveName): badWaveNameMessage = QMessageBox() badWaveNameMessage.setText("Wave name is not allowed or already in use. Please change the wave name.") badWaveNameMessage.exec_() return False # Get data type and size uiDataType = Util.getWidgetValue(self._ui.dataType) uiByteOrder = Util.getWidgetValue(self._ui.byteOrder) dataType = self.dataTypes[uiDataType]['dtype'] dataTypeChar = self.dataTypes[uiDataType]['char'] numBytes = self.dataTypes[uiDataType]['numbytes'] byteOrder = self.byteOrders[uiByteOrder] # Load data fileName = Util.getWidgetValue(self._ui.fileName) if os.path.isfile(fileName): data = [] wave = Wave(str(validatedWaveName), dataType) self._app.waves().addWave(wave) fh = open(fileName, 'rb') binDatum = fh.read(numBytes) while binDatum != "": try: datum = struct.unpack(byteOrder + dataTypeChar, binDatum) wave.push(datum[0]) except: pass binDatum = fh.read(numBytes) # data = array.array(dataTypeChar) # numDataPoints = os.path.getsize(fileName) / data.itemsize # fh = open(fileName, 'rb') # data.fromfile(fh, numDataPoints) # # wave = Wave(str(validatedWaveName), dataType) # wave.replaceData(data) # self._app.waves().addWave(wave) # Close window self.window.hide() def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionImportBinary") self.menuEntry.setShortcut("Ctrl+B") self.menuEntry.setText("Import Binary Data") self.menuEntry.triggered.connect(self.window.show) # Check if menu already exists if "menuImport" not in vars(self._app.ui).keys(): self._app.ui.menuImport = QMenu(self._app.ui.menuFile) self._app.ui.menuImport.setObjectName("menuImport") self._app.ui.menuImport.setTitle(QApplication.translate("MainWindow", "Import", None, QApplication.UnicodeUTF8)) self._app.ui.menuFile.addAction(self._app.ui.menuImport.menuAction()) self.menu = vars(self._app.ui)["menuImport"] self.menu.addAction(self.menuEntry) self.buildWidget() self.window.setWidget(self._widget) self._widget.setParent(self.window) self.window.hide() def unload(self): self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry)