Source code for sas.qtgui.Utilities.FileConverter

# pylint: disable=C0103, I1101
"""
File Converter Widget
"""
import os
import logging

import numpy as np

from PyQt5 import QtWidgets, QtCore, QtGui

from sas.sascalc.file_converter.ascii2d_loader import ASCII2DLoader
from sas.sascalc.file_converter.nxcansas_writer import NXcanSASWriter
from sas.sascalc.dataloader.data_info import Data1D

from sas.sascalc.dataloader.data_info import Detector
from sas.sascalc.dataloader.data_info import Sample
from sas.sascalc.dataloader.data_info import Source
from sas.sascalc.dataloader.data_info import Vector

import sas.sascalc.file_converter.FileConverterUtilities as Utilities

import sas.qtgui.Utilities.GuiUtils as GuiUtils
from sas.qtgui.Utilities.FrameSelect import FrameSelect

from sas.sasview import __version__ as SASVIEW_VERSION

from .UI.FileConverterUI import Ui_FileConverterUI

[docs]class FileConverterWidget(QtWidgets.QDialog, Ui_FileConverterUI): """ Class to describe the behaviour of the File Converter widget """ def __init__(self, parent=None): """ Parent here is the GUI Manager. Required for access to the help location and to the file loader. """ super(FileConverterWidget, self).__init__() self.parent = parent self.setupUi(self) # disable the context help icon self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) self.setWindowTitle("File Converter") # i,q file fields are not editable self.txtIFile.setEnabled(False) self.txtQFile.setEnabled(False) self.cmdConvert.setEnabled(False) # globals self.is1D = True self.isBSL = False self.ifile = "" self.qfile = "" self.ofile = "" self.metadata = {} self.setValidators() self.addSlots()
[docs] def setValidators(self): """ Apply validators for double precision numbers to numerical fields """ #self.txtMG_RunNumber.setValidator(QtGui.QIntValidator()) self.txtMD_Distance.setValidator(GuiUtils.DoubleValidator()) self.txtMD_OffsetX.setValidator(GuiUtils.DoubleValidator()) self.txtMD_OffsetY.setValidator(GuiUtils.DoubleValidator()) self.txtMD_OrientRoll.setValidator(GuiUtils.DoubleValidator()) self.txtMD_OrientPitch.setValidator(GuiUtils.DoubleValidator()) self.txtMD_OrientYaw.setValidator(GuiUtils.DoubleValidator()) self.txtMD_PixelX.setValidator(GuiUtils.DoubleValidator()) self.txtMD_PixelY.setValidator(GuiUtils.DoubleValidator()) self.txtMD_BeamX.setValidator(GuiUtils.DoubleValidator()) self.txtMD_BeamY.setValidator(GuiUtils.DoubleValidator()) self.txtMD_SlitLength.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_Thickness.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_Transmission.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_Temperature.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_PositionX.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_PositionY.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_OrientR.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_OrientY.setValidator(GuiUtils.DoubleValidator()) self.txtMSa_OrientP.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_BeamSizeX.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_BeamSizeY.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_BeamWavelength.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_MinWavelength.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_MaxWavelength.setValidator(GuiUtils.DoubleValidator()) self.txtMSo_Spread.setValidator(GuiUtils.DoubleValidator())
[docs] def addSlots(self): """ Create callbacks for UI elements and outside signals """ self.cmdConvert.clicked.connect(self.onConvert) self.cmdClose.clicked.connect(self.accept) self.cmdHelp.clicked.connect(self.onHelp) self.btnQFile.clicked.connect(self.onQFileOpen) self.btnIFile.clicked.connect(self.onIFileOpen) self.btnOutputFile.clicked.connect(self.onNewFile) self.txtOutputFile.editingFinished.connect(self.onNewFileEdited) self.cbInputFormat.currentIndexChanged.connect(self.onInputFormat)
[docs] def onConvert(self): """ Call the conversion method (and update DataExplorer with converted data)? """ self.readMetadata() try: if not self.isBSL and self.is1D: qdata = Utilities.extract_ascii_data(self.qfile) iqdata = np.array([Utilities.extract_ascii_data(self.ifile)]) self.convert1Ddata(qdata, iqdata, self.ofile, self.getMetadata()) elif self.isBSL and self.is1D: qdata, iqdata = Utilities.extract_otoko_data(self.qfile, self.ifile) self.convert1Ddata(qdata, iqdata, self.ofile, self.getMetadata()) elif not self.isBSL and not self.is1D: loader = ASCII2DLoader(self.ifile) data = loader.load() dataset = [data] # ASCII 2D only ever contains 1 frame Utilities.convert_2d_data(dataset, self.ofile, self.getMetadata()) else: # self.data_type == 'bsl' #dataset = Utilities.extract_bsl_data(self.ifile) dataset = self.extractBSLdata(self.ifile) if dataset is None: return Utilities.convert_2d_data(dataset, self.ofile, self.getMetadata()) except (ValueError, IOError) as ex: msg = str(ex) logging.error(msg) return # everything converted, notify the user logging.info("File successfully converted.") self.parent.communicate.statusBarUpdateSignal.emit("File converted successfully.") # Optionally, load the freshly converted file into Data Explorer if self.chkLoadFile.isChecked(): # awful climbing up the hierarchy... don't do that. please. self.parent.filesWidget.loadFromURL([self.ofile])
[docs] def onHelp(self): """ Display online help related to the file converter """ location = "/user/qtgui/Calculators/file_converter_help.html" self.parent.showHelp(location)
[docs] def onIFileOpen(self): """ Show the path chooser for file with I """ file_candidate = self.openFile() if not file_candidate: return self.ifile = file_candidate self.txtIFile.setText(os.path.basename(str(file_candidate))) self.updateConvertState()
[docs] def onQFileOpen(self): """ Show the path chooser for file with Q """ file_candidate = self.openFile() if not file_candidate: return self.qfile = file_candidate self.txtQFile.setText(os.path.basename(str(file_candidate))) self.updateConvertState()
[docs] def openFile(self): """ Show the path chooser for existent file """ datafile = None try: datafile = QtWidgets.QFileDialog.getOpenFileName( self, "Choose a file", "", "All files (*.*)")[0] except (RuntimeError, IOError) as ex: log_msg = "File Converter failed with: {}".format(ex) logging.error(log_msg) raise return datafile
[docs] def getDetectorMetadata(self): """ Read the detector metadata fields and put them in the dictionary """ detector = Detector() detector.name = self.txtMD_Name.text() detector.distance = Utilities.toFloat(self.txtMD_Distance.text()) detector.offset = Vector(x=Utilities.toFloat(self.txtMD_OffsetX.text()), y=Utilities.toFloat(self.txtMD_OffsetY.text())) detector.orientation = Vector(x=Utilities.toFloat(self.txtMD_OrientRoll.text()), y=Utilities.toFloat(self.txtMD_OrientPitch.text()), z=Utilities.toFloat(self.txtMD_OrientYaw.text())) detector.beam_center = Vector(x=Utilities.toFloat(self.txtMD_BeamX.text()), y=Utilities.toFloat(self.txtMD_BeamY.text())) detector.pixel_size = Vector(x=Utilities.toFloat(self.txtMD_PixelX.text()), y=Utilities.toFloat(self.txtMD_PixelY.text())) detector.slit_length = Utilities.toFloat(self.txtMD_Distance.text()) return detector
[docs] def getSourceMetadata(self): """ Read the source metadata fields and put them in the dictionary """ source = Source() # radiation is on the front panel source.radiation = self.cbRadiation.currentText().lower() # the rest is in the 'Source' tab of the Metadata tab source.name = self.txtMSo_Name.text() source.beam_size = Vector(x=Utilities.toFloat(self.txtMSo_BeamSizeX.text()), y=Utilities.toFloat(self.txtMSo_BeamSizeY.text())) source.beam_shape = self.txtMSo_BeamShape.text() source.wavelength = Utilities.toFloat(self.txtMSo_BeamWavelength.text()) source.wavelength_min = Utilities.toFloat(self.txtMSo_MinWavelength.text()) source.wavelength_max = Utilities.toFloat(self.txtMSo_MaxWavelength.text()) source.wavelength_spread = Utilities.toFloat(self.txtMSo_Spread.text()) return source
[docs] def getSampleMetadata(self): """ Read the sample metadata fields and put them in the dictionary """ sample = Sample() sample.name = self.txtMSa_Name.text() sample.thickness = Utilities.toFloat(self.txtMSa_Thickness.text()) sample.transmission = Utilities.toFloat(self.txtMSa_Transmission.text()) sample.temperature = Utilities.toFloat(self.txtMSa_Temperature.text()) sample.temperature_unit = self.txtMSa_TempUnit.text() sample.position = Vector(x=Utilities.toFloat(self.txtMSa_PositionX.text()), y=Utilities.toFloat(self.txtMSa_PositionY.text())) sample.orientation = Vector(x=Utilities.toFloat(self.txtMSa_OrientR.text()), y=Utilities.toFloat(self.txtMSa_OrientP.text()), z=Utilities.toFloat(self.txtMSa_OrientY.text())) details = self.txtMSa_Details.toPlainText() sample.details = [details] if details else [] return sample
[docs] def getMetadata(self): ''' metadata getter ''' return self.metadata
[docs] def readMetadata(self): """ Read the metadata fields and put them in the dictionary This reads the UI elements directly, but we don't have a clear MVP distinction in this widgets, so there. """ run_title = self.txtMG_RunName.text() run = self.txtMG_RunNumber.text() run = run.split(",") run_name = None if run: run_number = run[0] run_name = { run_number: run_title } metadata = { 'title': self.txtMG_Title.text(), 'run': run, 'run_name': run_name, # if run_name != "" else "None" , 'instrument': self.txtMG_Instrument.text(), 'detector': [self.getDetectorMetadata()], 'sample': self.getSampleMetadata(), 'source': self.getSourceMetadata(), 'notes': [f'Data file generated by SasView v{SASVIEW_VERSION}'], } self.metadata = metadata
[docs] def onNewFile(self): """ show the save new file widget """ wildcard1d = "CanSAS 1D files(*.xml);;" if self.is1D else "" wildcard = wildcard1d + "NXcanSAS files (*.h5)" kwargs = { 'caption' : 'Save As', 'filter' : wildcard, 'parent' : None, 'options' : QtWidgets.QFileDialog.DontUseNativeDialog } # Query user for filename. filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs) filename = filename_tuple[0] # User cancelled. if not filename: return # Check/add extension if not os.path.splitext(filename)[1]: ext = filename_tuple[1] if 'CanSAS' in ext: filename += '.xml' elif 'NXcanSAS' in ext: filename += '.h5' else: filename += '.h5' # default for user entered filenames self.ofile = filename self.txtOutputFile.setText(filename) self.updateConvertState()
[docs] def onNewFileEdited(self): """ Update the output file state on direct field edit """ text = self.txtOutputFile.text() if not text: return # Check/add extension filename_tuple = os.path.splitext(text) ext = filename_tuple[1] if not ext.lower() in ('.xml', '.h5'): text += '.h5' if not self.is1D and not '.h5' in ext.lower(): # quietly add .h5 as extension text += '.h5' self.ofile = text self.updateConvertState()
[docs] def updateConvertState(self): """ Asserts presece of files for coversion. If all present -> enable the Convert button. """ enabled = self.ifile != "" and os.path.exists(self.ifile) and self.ofile != "" if self.is1D: enabled = enabled and self.qfile != "" and os.path.exists(self.qfile) self.cmdConvert.setEnabled(enabled)
[docs] def onInputFormat(self): """ Enable/disable UI items based on input format spec """ # ASCII 2D allows for one file only self.is1D = not '2D' in self.cbInputFormat.currentText() self.label_7.setVisible(self.is1D) self.txtQFile.setVisible(self.is1D) self.btnQFile.setVisible(self.is1D) self.isBSL = 'BSL' in self.cbInputFormat.currentText() # clear out filename fields self.txtQFile.setText("") self.txtIFile.setText("")
# No need to clear the output field.
[docs] def extractBSLdata(self, filename): """ Extracts data from a 2D BSL file :param filename: The header file to extract the data from :return x_data: A 1D array containing all the x coordinates of the data :return y_data: A 1D array containing all the y coordinates of the data :return frame_data: A dictionary of the form *{frame_number: data}*, where data is a 2D numpy array containing the intensity data """ loader = Utilities.BSLLoader(filename) frames = [0] should_continue = True if loader.n_frames > 1: params = self.askFrameRange(loader.n_frames) frames = params['frames'] if len(frames) == 0: should_continue = False elif loader.n_rasters == 1 and loader.n_frames == 1: message = ("The selected file is an OTOKO file. Please select the " "'OTOKO 1D' option if you wish to convert it.") msgbox = QtWidgets.QMessageBox(self) msgbox.setIcon(QtWidgets.QMessageBox.Warning) msgbox.setText(message) msgbox.setWindowTitle("File Conversion") msgbox.exec_() return else: msg = ("The selected data file only has 1 frame, it might be" " a multi-frame OTOKO file.\nContinue conversion?") msgbox = QtWidgets.QMessageBox(self) msgbox.setIcon(QtWidgets.QMessageBox.Warning) msgbox.setText(msg) msgbox.setWindowTitle("File Conversion") # custom buttons button_yes = QtWidgets.QPushButton("Yes") msgbox.addButton(button_yes, QtWidgets.QMessageBox.YesRole) button_no = QtWidgets.QPushButton("No") msgbox.addButton(button_no, QtWidgets.QMessageBox.RejectRole) retval = msgbox.exec_() if retval == QtWidgets.QMessageBox.RejectRole: # cancel fit return if not should_continue: return None frame_data = loader.load_frames(frames) return frame_data
[docs] def convert1Ddata(self, qdata, iqdata, ofile, metadata): """ Formats a 1D array of q_axis data and a 2D array of I axis data (where each row of iqdata is a separate row), into an array of Data1D objects """ frames = [] increment = 1 single_file = True n_frames = iqdata.shape[0] # Standard file has 3 frames: SAS, calibration and WAS if n_frames > 3: # File has multiple frames - ask the user which ones they want to # export params = self.askFrameRange(n_frames) frames = params['frames'] increment = params['inc'] single_file = params['file'] if frames == []: return else: # Only interested in SAS data frames = [0] output_path = ofile frame_data = {} for i in frames: data = Data1D(x=qdata, y=iqdata[i]) frame_data[i] = data if single_file: # Only need to set metadata on first Data1D object frame_data = list(frame_data.values()) # Don't need to know frame numbers frame_data[0].filename = output_path.split('\\')[-1] for key, value in metadata.items(): setattr(frame_data[0], key, value) else: # Need to set metadata for all Data1D objects for datainfo in list(frame_data.values()): datainfo.filename = output_path.split('\\')[-1] for key, value in metadata.items(): setattr(datainfo, key, value) _, ext = os.path.splitext(output_path) if ext == '.xml': run_name = metadata['title'] Utilities.convert_to_cansas(frame_data, output_path, run_name, single_file) else: # ext == '.h5' w = NXcanSASWriter() w.write(frame_data, output_path)
[docs] def askFrameRange(self, n_frames=1): """ Display a dialog asking the user to input the range of frames they would like to export :param n_frames: How many frames the loaded data file has :return: A dictionary containing the parameters input by the user """ valid_input = False output_path = self.txtOutputFile.text() if not output_path: return _, ext = os.path.splitext(output_path) show_single_btn = (ext == '.h5') frames = None increment = None single_file = True dlg = FrameSelect(self, n_frames, show_single_btn) if dlg.exec_() != QtWidgets.QDialog.Accepted: return (first_frame, last_frame, increment) = dlg.getFrames() frames = list(range(first_frame, last_frame + 1, increment)) return { 'frames': frames, 'inc': increment, 'file': single_file }