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 Preferences(): """ Store preferences locally for easy programmatic access. Persistant copy of preferences will be stored in a file and loaded upon startup. User can modify preferences by opening UI, editing preferences, and saving them. By saving the preferences, the object will be updated to reflect the UI, and the file will also be updated. """ # These are the defaults for each preference. They will be overwritten # by the user's preferences at startup. prefs = { 'projectDirectory': Property.String('~/'), # Dir for where to store project files 'defaultDirectory': Property.String('~/'), # Dir for other purposes 'textOptions': Property.TextOptions(), # Default text options } def __init__(self, fileName): self._app = QApplication.instance().window self.preferencesFile = os.path.expanduser(fileName) self._widget = QWidget() self._ui = Ui_Preferences() self._ui.setupUi(self._widget) # Load preferences from file self.loadPreferencesFile() self.resetUi() # Connect button signals self._ui.buttons.button(QDialogButtonBox.Reset).clicked.connect(self.resetUi) self._ui.buttons.button(QDialogButtonBox.Save).clicked.connect(self.savePreferences) self._ui.buttons.button(QDialogButtonBox.Cancel).clicked.connect(self.hideDialog) self._ui.defaultDirectoryButton.clicked.connect(self.defaultDirectorySelector) self._ui.projectDirectoryButton.clicked.connect(self.projectDirectorySelector) #self._ui.textOptions.clicked.connect(self._ui.textOptions.showTextOptionsDialog) self._window = SubWindow(self._app.ui.workspace) self._window.setWidget(self._widget) self._widget.setParent(self._window) self._window.hide() def getInternal(self, variable): try: return self.prefs[variable].get() except: return None def getUi(self, variable): return Util.getWidgetValue(vars(self._ui)[variable]) def setInternal(self, variable, value): return self.prefs[variable].set(value) def setUi(self, variable, value): return Util.setWidgetValue(vars(self._ui)[variable], value) def showDialog(self): self.resetUi() self._window.show() def hideDialog(self): self.resetUi() self._window.hide() def loadPreferencesFile(self): if os.path.isfile(self.preferencesFile): with open(self.preferencesFile, 'r') as fileHandle: self.prefs = pickle.load(fileHandle) def uiToInternal(self): """ Copy the UI pref values to the internal prefs dict. """ for pref in self.prefs.keys(): self.setInternal(pref, self.getUi(pref)) def resetUi(self): """ Update UI to the current internal cache of preferences. """ for pref in self.prefs.keys(): self.setUi(pref, self.getInternal(pref)) def savePreferences(self): """ Save preferences to file and then read file to internal cache. Close dialog afterwards. """ self.uiToInternal() self.writePreferences() self.loadPreferencesFile() self.hideDialog() def writePreferences(self): if os.path.isfile(self.preferencesFile) or not os.path.exists(self.preferencesFile): with open(self.preferencesFile, 'wb') as fileHandle: pickle.dump(self.prefs, fileHandle) # Methods for specific preferences def defaultDirectorySelector(self): directory = QFileDialog.getExistingDirectory(self._app.ui.workspace, "Select Default Directory", Util.getWidgetValue(self._ui.defaultDirectory)) if os.path.isdir(directory): return self.setUi('defaultDirectory', directory) return False def projectDirectorySelector(self): directory = QFileDialog.getExistingDirectory(self._app.ui.workspace, "Select Project Directory", Util.getWidgetValue(self._ui.projectDirectory)) if os.path.isdir(directory): return self.setUi('projectDirectory', directory) return False
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)
class EditFigureDialog(Module): """Module to display the Edit Figure dialog window.""" def __init__(self): Module.__init__(self) def buildWidget(self): """Create the widget and populate it.""" self._currentFigure = None # Create enclosing widget and UI self._widget = QWidget() self._ui = Ui_EditFigureDialog() self._ui.setupUi(self._widget) self.setModels() # Connect signals for buttons at top of widget self._ui.addFigureButton.clicked.connect(self.createFigure) self._ui.showFigureButton.clicked.connect(self.showFigure) self._ui.deleteFigureButton.clicked.connect(self.deleteFigure) # Setup figure tab self._figureTab = QFigureOptionsWidget(self._widget) figureOW = Ui_FigureOptionsWidget() figureOW.setupUi(self._figureTab) self._figureTab.setUiObject(figureOW) self._figureTab.setEditFigureDialogModule(self) self._ui.tabWidget.insertTab(0, self._figureTab, "Figure") # Setup plot tab self._plotTab = QPlotOptionsWidget(self._widget) plotOW = Ui_PlotOptionsWidget() plotOW.setupUi(self._plotTab) self._plotTab.setUiObject(plotOW) self._plotTab.setEditFigureDialogModule(self) self._plotTab.initPlotSelector() self._plotTab.initSubWidgets() self._ui.tabWidget.insertTab(1, self._plotTab, "Plot") # If the number of rows or columns is changed, then we need to refresh the plot list # It's simpler to just always check this when the Apply button is pressed figureOW.buttons.button(QDialogButtonBox.Apply).clicked.connect(self._plotTab.refreshPlotSelector) # Make sure that the figure tab is initially displayed self._ui.tabWidget.setCurrentIndex(0) def setModels(self): # Setup figure list self._figureListModel = FigureListModel(self._app.figures()) self._ui.figureSelector.setModel(self._figureListModel) self._ui.figureSelector.currentIndexChanged[int].connect(self.changeCurrentFigure) self._app.figures().figureAdded.connect(self._figureListModel.doReset) self._app.figures().figureRemoved.connect(self._figureListModel.doReset) self._app.figures().figureRenamed.connect(self.updateFigureSelectorBox) # # Handlers for the three buttons at the top of the widget # def createFigure(self): figure = Figure("NewFigure") self._app.figures().addFigure(figure) # Select figure self._ui.figureSelector.setCurrentIndex(self._ui.figureSelector.model().rowCount() - 1) def showFigure(self): """Display the figure, in case it got hidden.""" if self.currentFigure(): self.currentFigure().showFigure() def deleteFigure(self): """ Delete button has been pressed. Make sure the user really wants to delete the figure. """ figure = self.currentFigure() # Make sure we are on a valid figure if not figure: return False # Ask user if they really want to delete the figure questionMessage = QMessageBox() questionMessage.setIcon(QMessageBox.Question) questionMessage.setText("You are about to delete a figure.") questionMessage.setInformativeText("Are you sure this is what you want to do?") questionMessage.setStandardButtons(QMessageBox.Yes | QMessageBox.No) questionMessage.setDefaultButton(QMessageBox.No) answer = questionMessage.exec_() if answer == QMessageBox.Yes: figure.hideFigure() self._app.figures().removeFigure(figure) self.changeCurrentFigure(0) self.updateFigureSelectorIndex(0) # # Get current items # def currentFigure(self): return self._currentFigure def changeCurrentFigure(self, index): if index < 0: return self._currentFigure = self._ui.figureSelector.model().getFigure(index) self._figureTab.resetUi() self._plotTab.setFigure(self._currentFigure) # Update figure selector combo boxes def updateFigureSelectorBox(self): # Figure name may have changed, so we'll reset the figure combo box # But keep the same figure selected figure = self.currentFigure() # We are not actually changing the current figure, so we will disconnect this signal for the moment self._ui.figureSelector.currentIndexChanged[int].disconnect(self.changeCurrentFigure) currentFigureIndex = self._ui.figureSelector.currentIndex() self._ui.figureSelector.model().doReset() self._ui.figureSelector.setCurrentIndex(currentFigureIndex) self._ui.figureSelector.currentIndexChanged[int].connect(self.changeCurrentFigure) def updateFigureSelectorIndex(self, index): """ Programmatically change the selected entry in the figure selector box. This is useful for deleting figures and also for loading a project. """ if index < 0: return self._ui.figureSelector.setCurrentIndex(index) """Module-required methods""" def getMenuNameToAddTo(self): return "menuPlot" def prepareMenuItem(self, menu): menu.setObjectName("actionEditFiguresDialog") menu.setShortcut("Ctrl+E") menu.setText("Edit Figures") return menu def load(self): self.window = SubWindow(self._app.ui.workspace) self.menuEntry = QAction(self._app) self.menuEntry.setObjectName("actionEditFiguresDialog") self.menuEntry.setShortcut("Ctrl+F") self.menuEntry.setText("Edit Figures") self.menuEntry.triggered.connect(self.window.show) self.menu = vars(self._app.ui)["menuPlot"] 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._app.figures().figureAdded.disconnect(self._figureListModel.doReset) self._app.figures().figureRemoved.disconnect(self._figureListModel.doReset) self.menuEntry.triggered.disconnect() self._widget.deleteLater() self.window.deleteLater() self.menu.removeAction(self.menuEntry) def reload(self): self.setModels() self._plotTab.reload() self._figureTab.reload()