#------------------------------------------------------------------------------- # Name: ApplicationWindow # Purpose: # # Author: Kelvin # # Created: 02/01/2014 #------------------------------------------------------------------------------- from PyQt4 import QtGui, QtCore from Skymap import * from SpectralGraph import * from ProgressBar import * from DuchampResults import * import pyfits import os """Application Window Class Definition""" # This handles the main window. class ApplicationWindow(QtGui.QMainWindow): # Main Window Initialisation def __init__(self): # QMainWindow Initialisation QtGui.QMainWindow.__init__(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setWindowTitle("Duchamp") # File Menu self.file_menu = QtGui.QMenu('&File', self) self.file_menu.addAction('&Open File', self.openFiles, QtCore.Qt.CTRL + QtCore.Qt.Key_O) self.file_menu.addAction('&Save as parameters file', self.saveParameters, QtCore.Qt.CTRL + QtCore.Qt.Key_P) self.file_menu.addSeparator() self.file_menu.addAction('&Quit', self.close, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) self.menuBar().addMenu(self.file_menu) # Options Menu self.options_menu = QtGui.QMenu('&Options', self) self.menuBar().addSeparator() self.menuBar().addMenu(self.options_menu) self.options_menu.addAction('&Full Screen Mode', self.showFullScreen, QtCore.Qt.CTRL + QtCore.Qt.Key_F) self.options_menu.addAction('&Maximized Mode', self.showMaximized, QtCore.Qt.CTRL + QtCore.Qt.Key_M) self.options_menu.addAction('&Normal Mode', self.showNormal, QtCore.Qt.CTRL + QtCore.Qt.Key_N) # Help Menu self.help_menu = QtGui.QMenu('&Help', self) self.menuBar().addSeparator() self.menuBar().addMenu(self.help_menu) self.help_menu.addAction('&About', self.about, QtCore.Qt.CTRL + QtCore.Qt.Key_A) # Main Widget self.main_widget = QtGui.QWidget(self) self.main_widget.setFocus() self.setCentralWidget(self.main_widget) self.layout = QtGui.QGridLayout(self.main_widget) # Main Window Status Bar self.statusBar().showMessage("Duchamp Output Window", 2000) # Window location and size self.center = QtGui.QDesktopWidget().availableGeometry().center() self.move(0, 0) self.xsize = 2*self.center.x()*0.9 self.ysize = 2*self.center.y()*0.9 self.resize(self.xsize, self.ysize) # About Message def about(self): QtGui.QMessageBox.about(self, "About Duchamp", """ Duchamp is an object-finder for use on spectral-line data cubes. The basic execution of Duchamp is to read in a FITS data cube, find sources in the cube, and produce a text file of positions, velocities and fluxes of the detections, as well as a postscript file of the spectra of each detection. Duchamp has been designed to search for objects in particular sorts of data: those with relatively small, isolated objects in a large amount of background or noise. Examples of such data are extragalactic HI surveys, or maser surveys. Duchamp searches for groups of connected voxels (or pixels) that are all above some flux threshold. No assumption is made as to the shape of detections, and the only size constraints applied are those specified by the user. Duchamp has been written as a three-dimensional source finder, but it is possible to run it on a two-dimensional image (i.e. one with no frequency or velocity information), or indeed a one-dimensional array, and many of the features of the program will work fine. The focus, however, is on object detection in three dimensions, one of which is a spectral dimension. Note, in particular, that it does not do any fitting of source profiles, a feature common (and desirable) for many two-dimensional source finders. This is beyond the current scope of Duchamp, whose aim is reliable detection of spectral-line objects. Duchamp provides the ability to pre-process the data prior to searching. Spectral baselines can be removed, and either smoothing or multi-resolution wavelet reconstruction can be performed to enhance the completeness and reliability of the resulting catalogue. """) # This prompts the user to open required files. def openFiles(self): # Prompt for results file name. resultsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open results File', '.', "text files (*.txt);;All files (*)") # If the name is empty, then we did not load any files. if not resultsFileName: QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok) return # Open up the results file. resultsFile = open(resultsFileName) # Get the first line of the results file. for line in resultsFile: firstline = line break # Determine if the content of the results file is correct. if "# Results of the Duchamp source finder" not in firstline: QtGui.QMessageBox.information(self, 'Message', 'Incorrect content for results file. No file loaded.', QtGui.QMessageBox.Ok) return # Find the maskfits and fits file name. maskfitsFileName = '' fitsFileName = '' for line in resultsFile: if 'imageFile' in line and ' = ' in line: fitsFileName = line.split(' = ')[-1].split()[0].strip() if 'flagOutputMask' in line and '>' in line: maskfitsFileName = line.split('>')[-1].split()[0].strip() # If we cannot find the fits file name, prompt the user to find it. if not fitsFileName: QtGui.QMessageBox.information(self, 'Message', 'Results file do not contain input "imageFile" name. Please specify the original input fits file used in source finding.', QtGui.QMessageBox.Ok) # Prompt for the input cube fits file name. fitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open input File', '.', "fits files (*.fits);;All files (*)") # If the name is empty, then we did not load any files. if not fitsFileName: QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok) return # If the file found does not exist, then let the user know and give the user a final chance. while not os.path.isfile(fitsFileName): QtGui.QMessageBox.information(self, 'Message', 'Input "imageFile" <%s> specified in results file cannot be found in current directory. Please specify the original input fits file used in source finding.'%fitsFileName, QtGui.QMessageBox.Ok) # Prompt for the input cube fits file name. fitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open input File', '.', "fits files (*.fits);;All files (*)") # If the user cancelled the loading, then do not prompt the user anymore. if not fitsFileName: return # If we cannot find the maskfits file name, prompt the user to find it. if not maskfitsFileName: QtGui.QMessageBox.information(self, 'Message', 'Results file do not contain MASK fits file name. Please specify MASK fits file.', QtGui.QMessageBox.Ok) # Prompt for the mask cube fits file name. maskfitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open mask File', '.', "MASK fits files (*.MASK.fits);;fits files (*.fits);;All files (*)") # If the name is empty, then we did not load any files. if not maskfitsFileName: QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok) return # If the file found does not exist, then let the user know and give the user a final chance. while not os.path.isfile(maskfitsFileName): QtGui.QMessageBox.information(self, 'Message', 'MASK fits file <%s> specified in results file cannot be found in current directory. Please specify the MASK fits file.'%maskfitsFileName, QtGui.QMessageBox.Ok) # Prompt for the mask cube fits file name. maskfitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open mask File', '.', "MASK fits files (*.MASK.fits);;fits files (*.fits);;All files (*)") # If the user cancelled the loading, then do not prompt the user anymore. if not maskfitsFileName: return # Store the results from the results file as a class self.Results = DuchampResults(resultsFileName) # Set the names of the files self.Results.maskfitsFileName = maskfitsFileName self.Results.fitsFileName = fitsFileName # If the file names are okay, then check and load the files using those names. self.checkAndLoadFiles(maskfitsFileName, fitsFileName) # This makes sure that the files are correct and in the correct format. # If everything is good, it will start the software. def checkAndLoadFiles(self, maskfitsFileName, fitsFileName): # Try reading the files. try: # Open the mask cube fits file. maskFits = pyfits.open(maskfitsFileName) # If the 'BUNIT' option in the mask cube fits header is not 'Object ID', then the program will not start. if maskFits[0].header.get('BUNIT') != 'Object ID': QtGui.QMessageBox.warning(self, 'Incorrect specification in .MASK.fits file', 'The "BUNIT" option in "%s" is not set to "Object ID". Files not loaded.'%maskfitsFileName, QtGui.QMessageBox.Ok) return(0) # Open the input cube fits file. realFits = pyfits.open(fitsFileName) # If the two cubes do not have matching shapes, then they are obviously not meant for each other. The program will not start. if maskFits[0].data.shape != realFits[0].data.shape: QtGui.QMessageBox.warning(self, 'Cube sizes mismatch', 'Fits cube sizes differ in shape in files "%s" and "%s". Files not loaded.'%(maskfitsFileName, fitsFileName), QtGui.QMessageBox.Ok) return(0) # If everything is fine, we will confirm the choice of files with the user. SuccessMsg = QtGui.QMessageBox() SuccessMsg.setWindowTitle("Files Loaded") SuccessMsg.setText('Files\n"%s" and \n"%s"\n are loaded successfully!'%(maskfitsFileName, fitsFileName)) SuccessMsg.setInformativeText('Please confirm that those are the correct files.') SuccessMsg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.Discard) SuccessMsg.setDefaultButton(QtGui.QMessageBox.Yes) # If the user is fine with the choice, then start the program with those files. if SuccessMsg.exec_() == QtGui.QMessageBox.Yes: # This is the name of the input file self.fitsName = fitsFileName.split('/')[-1] # This is the name of the maskfits file self.maskfitsName = maskfitsFileName.split('/')[-1] # Those are the fits data self.maskFits = maskFits self.realFits = realFits # Now we can start processing the data self.processData() return(1) # Otherwise, discard the files. QtGui.QMessageBox.information(self, 'Message', 'Files discarded.', QtGui.QMessageBox.Ok) return(0) # If anything went wrong in the above process, then file reading has failed. except Exception: QtGui.QMessageBox.warning(self, 'Error', 'Reading of fits files failed. Files not loaded.', QtGui.QMessageBox.Ok) return(0) # This loads the required data from the files that contains the data. def processData(self): # Use a progress bar to indicate the loading process self.pbar = ProgressBar(title = "Processing data", total = 100) self.pbar.show() # Obtain the two cubes self.maskCube = self.maskFits[0].data self.realCube = self.realFits[0].data # Those are the size of our data cubes [self.lenZ, self.lenY, self.lenX] = self.maskCube.shape # This is the cube used to produce the final image self.finalCube = zeros([self.lenZ, self.lenY, self.lenX]) # Those are our mask image, real image, and final image respectively self.maskImage = zeros([self.lenY, self.lenX]) self.realImage = zeros([self.lenY, self.lenX]) self.finalImage = zeros([self.lenY, self.lenX]) # This will contain all the moment maps with their index matching its object ID self.momentMaps = ['Moment Maps'] # Here we compute the final cube from the mask cube and real cube # Go through each object we found for ObjID in range(1, self.Results.totalDetections + 1): # Obtain information regarding that source ObjStats = self.Results.SkyObjects[ObjID].stats # Obtain the spatial and spectral bound of that source x1 = ObjStats['X1'] x2 = ObjStats['X2'] y1 = ObjStats['Y1'] y2 = ObjStats['Y2'] z1 = ObjStats['Z1'] z2 = ObjStats['Z2'] # Calculate the moment map of the source as well as add the source to the final cube momentMap = zeros([self.lenY, self.lenX]) for z in range(z1, z2 + 1): momentMap += self.realCube[z] for y in range(y1, y2 + 1): for x in range(x1, x2 + 1): if self.maskCube[z][y][x] == ObjID: self.finalCube[z][y][x] = self.realCube[z][y][x] # Add this moment map to the list of moment maps self.momentMaps += [momentMap] # And also compute the mask image from the mask cube for z in range(self.lenZ): self.maskImage[self.maskCube[z] == ObjID] = ObjID # Update the progress bar accordingly self.pbar.update_progressbar(100*ObjID/self.Results.totalDetections + 1) # Compute the real image from the real cube for z in range(self.lenZ): self.realImage += self.realCube[z] # Compute the final image from the final cube for z in range(self.lenZ): self.finalImage += self.finalCube[z] # The data processing has finished self.pbar.close() # The program can now start self.start() # This defines what happens when the software starts. def start(self): # Use a progress bar to indicate the start up progress self.pbar = ProgressBar(title = "Loading Skymap", total = 100) self.pbar.show() # Start the skymap. self.startSkymap() self.pbar.update_progressbar(26) # Start the spectral graph. self.startSpectralGraph() self.pbar.update_progressbar(51) # Start the zoom graph self.startZoomGraph() self.pbar.update_progressbar(76) # Start the moment map self.startMomentMap() self.pbar.update_progressbar(100) self.pbar.close() # This starts the skymap. def startSkymap(self): # Initialise the skymap. self.skymap = Skymap(self.Results, maskImage = self.maskImage, finalImage = self.finalImage) self.skymap.drawImage() self.skymap.enableInteractivity() self.skymap.fig.suptitle(self.fitsName) # Determine its size and location. self.skymap.setMinimumHeight(0.5*self.ysize) self.skymap.setMinimumWidth(0.3*self.xsize) self.layout.addWidget(self.skymap, 0, 0, 4, 4) # Connect the signal from skymap to update the spectral graph. self.skymap.graphSignal.connect(self.updateSpectralGraph) self.skymap.graphSignal.connect(self.updateZoomGraph) self.skymap.graphSignal.connect(self.updateMomentMap) # This starts the spectral graph. def startSpectralGraph(self): # Initialise the spectral graph. self.spectralGraph = SpectralGraph(self.Results, self.maskFits, self.realFits, self.skymap.maskImage) # Determine its size and location. self.spectralGraph.setMinimumHeight(0.3*self.ysize) self.spectralGraph.setMinimumWidth(self.xsize) self.layout.addWidget(self.spectralGraph, 4, 0, 2, 8) # Initialise the radio buttons for display options. self.displayMaxButton = QtGui.QRadioButton("Peak Detected Intensity") self.displaySumButton = QtGui.QRadioButton("Total Detected Intensity") # Determine its location. self.layout.addWidget(self.displayMaxButton, 0, 4, 1, 1) self.layout.addWidget(self.displaySumButton, 0, 5, 1, 1) # Determine its functionality. self.displayMaxButton.toggled.connect(self.spectralGraph.plotMax) self.displaySumButton.toggled.connect(self.spectralGraph.plotSum) self.displayMaxButton.setChecked(True) # This updates the spectral graph. def updateSpectralGraph(self): # Set the current object to plot the spectral graph with as the highlighted object in the skymap, and plot the spectral graph. self.spectralGraph.setCurrentObjID(self.skymap.highlightedObj) self.spectralGraph.plotSource() # This starts the zoom graph. def startZoomGraph(self): # Initialise the zoom graph self.zoomGraph = ZoomGraph(self.Results, self.maskFits, self.realFits, self.skymap.maskImage) # Determine its location self.layout.addWidget(self.zoomGraph, 1, 4, 3, 2) # Link the radio button functionalities to the zoom graph self.displayMaxButton.toggled.connect(self.zoomGraph.plotMax) self.displaySumButton.toggled.connect(self.zoomGraph.plotSum) # This updates the zoom graph. def updateZoomGraph(self): # Set the current object to plot the zoom graph with as the highlighted object in the skymap, and plot the zoom graph. self.zoomGraph.setCurrentObjID(self.skymap.highlightedObj) self.zoomGraph.plotSource() # This starts the moment map. def startMomentMap(self): # Initialise the moment map self.momentMap = MomentMap(self.Results, maskImage = self.maskImage, finalImage = self.finalImage, momentMaps = self.momentMaps) # Determine its location self.layout.addWidget(self.momentMap, 1, 6, 3, 2) # This updates the moment map. def updateMomentMap(self): # Set the current object to draw the moment map with as the highlighted object in the skymap, and draw the moment map self.momentMap.setCurrentObjID(self.skymap.highlightedObj) self.momentMap.drawImage() # This saves the parameters used to generate this output into a parameter file. def saveParameters(self): # Prompt for the name of the file. parameterFilePath = QtGui.QFileDialog.getSaveFileName(self, 'Save parameters as', '.', "text files (*.txt);;All files (*)") # Write the parameter. try: writeParamFile(parameterFilePath, self.Results.parameters, self.Results.parameterName) except Exceptions: pass