source: sasview/src/sas/qtgui/Utilities/GridPanel.py @ aea6bb7

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_sync_sascalc
Last change on this file since aea6bb7 was 2327399, checked in by wojciech, 6 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

  • Property mode set to 100644
File size: 19.3 KB
RevLine 
[3b3b40b]1import os
[d4dac80]2import sys
[3b3b40b]3import time
4import logging
5import webbrowser
6
[d4dac80]7from PyQt5 import QtCore, QtWidgets, QtGui
[3b3b40b]8
9import sas.qtgui.Utilities.GuiUtils as GuiUtils
[d4dac80]10from sas.qtgui.Plotting.PlotterData import Data1D
[3b3b40b]11from sas.qtgui.Utilities.UI.GridPanelUI import Ui_GridPanelUI
12
13
14class BatchOutputPanel(QtWidgets.QMainWindow, Ui_GridPanelUI):
15    """
16    Class for stateless grid-like printout of model parameters for mutiple models
17    """
[d4dac80]18    ERROR_COLUMN_CAPTION = " (Err)"
19    IS_WIN = (sys.platform == 'win32')
[fa762f4]20    windowClosedSignal = QtCore.pyqtSignal()
[3b3b40b]21    def __init__(self, parent = None, output_data=None):
22
[d4dac80]23        super(BatchOutputPanel, self).__init__(parent._parent)
[3b3b40b]24        self.setupUi(self)
25
26        self.parent = parent
27        if hasattr(self.parent, "communicate"):
28            self.communicate = parent.communicate
29
30        self.addToolbarActions()
31
32        # file name for the dataset
33        self.grid_filename = ""
34
[d4dac80]35        self.has_data = False if output_data is None else True
36        # Tab numbering
37        self.tab_number = 1
38
[99f8760]39        # save state
40        self.data_dict = {}
41
[d4dac80]42        # System dependent menu items
43        if not self.IS_WIN:
44            self.actionOpen_with_Excel.setVisible(False)
45
46        # list of QTableWidgets, indexed by tab number
47        self.tables = []
48        self.tables.append(self.tblParams)
49
[3b3b40b]50        # context menu on the table
51        self.tblParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
52        self.tblParams.customContextMenuRequested.connect(self.showContextMenu)
53
54        # Command buttons
55        self.cmdHelp.clicked.connect(self.onHelp)
[d4dac80]56        self.cmdPlot.clicked.connect(self.onPlot)
57
58        # Fill in the table from input data
59        self.setupTable(widget=self.tblParams, data=output_data)
60        if output_data is not None:
61            # Set a table tooltip describing the model
[3717470]62            model_name = output_data[0][0].model.id
[d4dac80]63            self.tabWidget.setTabToolTip(0, model_name)
64
65    def closeEvent(self, event):
66        """
67        Overwrite QDialog close method to allow for custom widget close
68        """
[fa762f4]69        # notify the parent so it hides this window
70        self.windowClosedSignal.emit()
[d4dac80]71        event.ignore()
[3b3b40b]72
73    def addToolbarActions(self):
74        """
75        Assing actions and callbacks to the File menu items
76        """
77        self.actionOpen.triggered.connect(self.actionLoadData)
78        self.actionOpen_with_Excel.triggered.connect(self.actionSendToExcel)
79        self.actionSave.triggered.connect(self.actionSaveFile)
80
81    def actionLoadData(self):
82        """
83        Open file load dialog and load a .csv file
84        """
85        datafile = QtWidgets.QFileDialog.getOpenFileName(
86            self, "Choose a file with results", "", "CSV files (*.csv)", None,
87            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
88
89        if not datafile:
90            logging.info("No data file chosen.")
91            return
92
93        with open(datafile, 'r') as csv_file:
94            lines = csv_file.readlines()
95
96        self.setupTableFromCSV(lines)
[d4dac80]97        self.has_data = True
98
99    def currentTable(self):
100        """
101        Returns the currently shown QTabWidget
102        """
103        return self.tables[self.tabWidget.currentIndex()]
[3b3b40b]104
105    def showContextMenu(self, position):
106        """
107        Show context specific menu in the tab table widget.
108        """
109        menu = QtWidgets.QMenu()
[d4dac80]110        rows = [s.row() for s in self.currentTable().selectionModel().selectedRows()]
[3b3b40b]111        num_rows = len(rows)
112        if num_rows <= 0:
113            return
114        # Find out which items got selected and in which row
115        # Select for fitting
116
117        self.actionPlotResults = QtWidgets.QAction(self)
118        self.actionPlotResults.setObjectName("actionPlot")
119        self.actionPlotResults.setText(QtCore.QCoreApplication.translate("self", "Plot selected fits."))
120
121        menu.addAction(self.actionPlotResults)
122
123        # Define the callbacks
[d4dac80]124        self.actionPlotResults.triggered.connect(self.onPlot)
[3b3b40b]125        try:
[d4dac80]126            menu.exec_(self.currentTable().viewport().mapToGlobal(position))
[3b3b40b]127        except AttributeError as ex:
128            logging.error("Error generating context menu: %s" % ex)
129        return
130
[c4c4957]131    def addTabPage(self, name=None):
[d4dac80]132        """
133        Add new tab page with QTableWidget
134        """
135        layout = QtWidgets.QVBoxLayout()
136        tab_widget = QtWidgets.QTableWidget(parent=self)
137        # Same behaviour as the original tblParams
138        tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
139        tab_widget.setAlternatingRowColors(True)
140        tab_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
141        tab_widget.setLayout(layout)
142        # Simple naming here.
143        # One would think naming the tab with current model name would be good.
144        # However, some models have LONG names, which doesn't look well on the tab bar.
145        self.tab_number += 1
[c4c4957]146        if name is not None:
147            tab_name = name
148        else:
149            tab_name = "Tab " + str(self.tab_number)
[d4dac80]150        # each table needs separate slots.
151        tab_widget.customContextMenuRequested.connect(self.showContextMenu)
152        self.tables.append(tab_widget)
153        self.tabWidget.addTab(tab_widget, tab_name)
154        # Make the new tab active
155        self.tabWidget.setCurrentIndex(self.tab_number-1)
156
157    def addFitResults(self, results):
158        """
159        Create a new tab with batch fitting results
160        """
[c4c4957]161        # pull out page name from results
162        page_name = None
163        if len(results)>=2:
164            if isinstance(results[-1], str):
165                page_name = results[-1]
166                _ = results.pop(-1)
167
[fa762f4]168        if self.has_data:
[c4c4957]169            self.addTabPage(name=page_name)
170        else:
171            self.tabWidget.setTabText(0, page_name)
[d4dac80]172        # Update the new widget
173        # Fill in the table from input data in the last/newest page
174        assert(self.tables)
175        self.setupTable(widget=self.tables[-1], data=results)
176        self.has_data = True
177
178        # Set a table tooltip describing the model
179        model_name = results[0][0].model.id
180        self.tabWidget.setTabToolTip(self.tabWidget.count()-1, model_name)
[99f8760]181        self.data_dict[page_name] = results
[d4dac80]182
[8b480d27]183    @classmethod
184    def onHelp(cls):
[3b3b40b]185        """
186        Open a local url in the default browser
187        """
[aed0532]188        url = "/user/qtgui/Perspectives/Fitting/fitting_help.html#batch-fit-mode"
[a8e6394]189        location = GuiUtils.HELP_DIRECTORY_LOCATION + url
190        if os.path.isdir(location) == False:
191            sas_path = os.path.abspath(os.path.dirname(sys.argv[0]))
192            location = sas_path + "/" + location
[3b3b40b]193        try:
[a8e6394]194            webbrowser.open('file://' + os.path.realpath(location))
[3b3b40b]195        except webbrowser.Error as ex:
196            logging.warning("Cannot display help. %s" % ex)
197
[d4dac80]198    def onPlot(self):
[3b3b40b]199        """
200        Plot selected fits by sending signal to the parent
201        """
[d4dac80]202        rows = [s.row() for s in self.currentTable().selectionModel().selectedRows()]
203        if not rows:
204            msg = "Nothing to plot!"
205            self.parent.communicate.statusBarUpdateSignal.emit(msg)
206            return
207        data = self.dataFromTable(self.currentTable())
[3b3b40b]208        # data['Data'] -> ['filename1', 'filename2', ...]
209        # look for the 'Data' column and extract the filename
210        for row in rows:
211            try:
212                filename = data['Data'][row]
213                # emit a signal so the plots are being shown
214                self.communicate.plotFromFilenameSignal.emit(filename)
215            except (IndexError, AttributeError):
216                # data messed up.
217                return
218
[8b480d27]219    @classmethod
220    def dataFromTable(cls, table):
[3b3b40b]221        """
222        Creates a dictionary {<parameter>:[list of values]} from the parameter table
223        """
224        assert(isinstance(table, QtWidgets.QTableWidget))
225        params = {}
226        for column in range(table.columnCount()):
227            value = [table.item(row, column).data(0) for row in range(table.rowCount())]
228            key = table.horizontalHeaderItem(column).data(0)
229            params[key] = value
230        return params
231
232    def actionSendToExcel(self):
233        """
234        Generates a .csv file and opens the default CSV reader
235        """
236        if not self.grid_filename:
237            import tempfile
238            tmpfile = tempfile.NamedTemporaryFile(delete=False, mode="w+", suffix=".csv")
239            self.grid_filename = tmpfile.name
[d4dac80]240            data = self.dataFromTable(self.currentTable())
[3b3b40b]241            t = time.localtime(time.time())
242            time_str = time.strftime("%b %d %H:%M of %Y", t)
[8b480d27]243            details = "File Generated by SasView "
[3b3b40b]244            details += "on %s.\n" % time_str
245            self.writeBatchToFile(data=data, tmpfile=tmpfile, details=details)
246            tmpfile.close()
247
248        try:
249            from win32com.client import Dispatch
250            excel_app = Dispatch('Excel.Application')
251            excel_app.Workbooks.Open(self.grid_filename)
252            excel_app.Visible = 1
253        except Exception as ex:
254            msg = "Error occured when calling Excel.\n"
255            msg += ex
256            self.parent.communicate.statusBarUpdateSignal.emit(msg)
257
258    def actionSaveFile(self):
259        """
260        Generate a .csv file and dump it do disk
261        """
262        t = time.localtime(time.time())
263        time_str = time.strftime("%b %d %H %M of %Y", t)
264        default_name = "Batch_Fitting_"+time_str+".csv"
265
266        wildcard = "CSV files (*.csv);;"
267        kwargs = {
268            'caption'   : 'Save As',
269            'directory' : default_name,
270            'filter'    : wildcard,
271            'parent'    : None,
272        }
273        # Query user for filename.
274        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
275        filename = filename_tuple[0]
276
277        # User cancelled.
278        if not filename:
279            return
[d4dac80]280        data = self.dataFromTable(self.currentTable())
[3b3b40b]281        details = "File generated by SasView\n"
282        with open(filename, 'w') as csv_file:
283            self.writeBatchToFile(data=data, tmpfile=csv_file, details=details)
284
285    def setupTableFromCSV(self, csv_data):
286        """
287        Create tablewidget items and show them, based on params
288        """
[d4dac80]289        # Is this an empty grid?
290        if self.has_data:
291            # Add a new page
292            self.addTabPage()
293            # Access the newly created QTableWidget
294            current_page = self.tables[-1]
295        else:
296            current_page = self.tblParams
[3b3b40b]297        # headers
298        param_list = csv_data[1].rstrip().split(',')
[d4dac80]299        # need to remove the 2 header rows to get the total data row number
300        rows = len(csv_data) -2
301        assert(rows > -1)
302        columns = len(param_list)
303        current_page.setColumnCount(columns)
304        current_page.setRowCount(rows)
305
[3b3b40b]306        for i, param in enumerate(param_list):
[d4dac80]307            current_page.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
[3b3b40b]308
309        # first - Chi2 and data filename
310        for i_row, row in enumerate(csv_data[2:]):
311            for i_col, col in enumerate(row.rstrip().split(',')):
[d4dac80]312                current_page.setItem(i_row, i_col, QtWidgets.QTableWidgetItem(col))
[3b3b40b]313
[d4dac80]314        current_page.resizeColumnsToContents()
[3b3b40b]315
[d4dac80]316    def setupTable(self, widget=None, data=None):
[3b3b40b]317        """
318        Create tablewidget items and show them, based on params
319        """
[d4dac80]320        # quietly leave is nothing to show
321        if data is None or widget is None:
322            return
323
324        # Figure out the headers
[3b3b40b]325        model = data[0][0]
[d4dac80]326
[a4b9b7a]327        disperse_params = list(model.model.dispersion.keys())
328        magnetic_params = model.model.magnetic_params
329        optimized_params = model.param_list
330        # Create the main parameter list
331        param_list = [m for m in model.model.params.keys() if (m not in model.model.magnetic_params and ".width" not in m)]
332
333        # add fitted polydisp parameters
334        param_list += [m+".width" for m in disperse_params if m+".width" in optimized_params]
335
336        # add fitted magnetic params
337        param_list += [m for m in magnetic_params if m in optimized_params]
[3b3b40b]338
339        # Check if 2D model. If not, remove theta/phi
[d4dac80]340        if isinstance(model.data.sas_data, Data1D):
[7879745]341            if 'theta' in param_list:
342                param_list.remove('theta')
343            if 'phi' in param_list:
344                param_list.remove('phi')
[3b3b40b]345
346        rows = len(data)
347        columns = len(param_list)
348
[d4dac80]349        widget.setColumnCount(columns+2) # add 2 initial columns defined below
350        widget.setRowCount(rows)
351
352        # Insert two additional columns
[3b3b40b]353        param_list.insert(0, "Data")
354        param_list.insert(0, "Chi2")
355        for i, param in enumerate(param_list):
[d4dac80]356            widget.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
[3b3b40b]357
[d4dac80]358        # dictionary of parameter errors for post-processing
359        # [param_name] = [param_column_nr, error_for_row_1, error_for_row_2,...]
360        error_columns = {}
[3b3b40b]361        # first - Chi2 and data filename
362        for i_row, row in enumerate(data):
363            # each row corresponds to a single fit
364            chi2 = row[0].fitness
365            filename = ""
366            if hasattr(row[0].data, "sas_data"):
367                filename = row[0].data.sas_data.filename
[d4dac80]368            widget.setItem(i_row, 0, QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(chi2, high=True)))
369            widget.setItem(i_row, 1, QtWidgets.QTableWidgetItem(str(filename)))
[3b3b40b]370            # Now, all the parameters
371            for i_col, param in enumerate(param_list[2:]):
372                if param in row[0].param_list:
373                    # parameter is on the to-optimize list - get the optimized value
374                    par_value = row[0].pvec[row[0].param_list.index(param)]
[d4dac80]375                    # parse out errors and store them for later use
376                    err_value = row[0].stderr[row[0].param_list.index(param)]
377                    if param in error_columns:
378                        error_columns[param].append(err_value)
379                    else:
380                        error_columns[param] = [i_col, err_value]
[3b3b40b]381                else:
382                    # parameter was not varied
383                    par_value = row[0].model.params[param]
[d4dac80]384
385                widget.setItem(i_row, i_col+2, QtWidgets.QTableWidgetItem(
[3b3b40b]386                    GuiUtils.formatNumber(par_value, high=True)))
387
[d4dac80]388        # Add errors
389        error_list = list(error_columns.keys())
390        for error_param in error_list[::-1]: # must be reverse to keep column indices
391            # the offset for the err column: +2 from the first two extra columns, +1 to append this column
392            error_column = error_columns[error_param][0]+3
393            error_values = error_columns[error_param][1:]
394            widget.insertColumn(error_column)
395
396            column_name = error_param + self.ERROR_COLUMN_CAPTION
397            widget.setHorizontalHeaderItem(error_column, QtWidgets.QTableWidgetItem(column_name))
398
399            for i_row, error in enumerate(error_values):
400                item = QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(error, high=True))
401                # Fancy, italic font for errors
402                font = QtGui.QFont()
403                font.setItalic(True)
404                item.setFont(font)
405                widget.setItem(i_row, error_column, item)
406
407        # resize content
408        widget.resizeColumnsToContents()
[3b3b40b]409
[8b480d27]410    @classmethod
411    def writeBatchToFile(cls, data, tmpfile, details=""):
[3b3b40b]412        """
413        Helper to write result from batch into cvs file
414        """
415        name = tmpfile.name
416        if data is None or name is None or name.strip() == "":
417            return
418        _, ext = os.path.splitext(name)
419        separator = "\t"
420        if ext.lower() == ".csv":
421            separator = ","
422        tmpfile.write(details)
423        for col_name in data.keys():
424            tmpfile.write(col_name)
425            tmpfile.write(separator)
426        tmpfile.write('\n')
427        max_list = [len(value) for value in data.values()]
428        if len(max_list) == 0:
429            return
430        max_index = max(max_list)
431        index = 0
432        while index < max_index:
433            for value_list in data.values():
434                if index < len(value_list):
435                    tmpfile.write(str(value_list[index]))
436                    tmpfile.write(separator)
437                else:
438                    tmpfile.write('')
439                    tmpfile.write(separator)
440            tmpfile.write('\n')
441            index += 1
442
[effdd98]443
444class BatchInversionOutputPanel(BatchOutputPanel):
445    """
446        Class for stateless grid-like printout of P(r) parameters for any number
447        of data sets
448    """
449    def __init__(self, parent = None, output_data=None):
450
[4fbf0db]451        super(BatchInversionOutputPanel, self).__init__(parent._parent, output_data)
[5a5e371]452        _translate = QtCore.QCoreApplication.translate
453        self.setWindowTitle(_translate("GridPanelUI", "Batch P(r) Results"))
[effdd98]454
[4fbf0db]455    def setupTable(self, widget=None,  data=None):
[effdd98]456        """
457        Create tablewidget items and show them, based on params
458        """
459        # headers
[5a5e371]460        param_list = ['Filename', 'Rg [Å]', 'Chi^2/dof', 'I(Q=0)', 'Oscillations',
461                      'Background [Å^-1]', 'P+ Fraction', 'P+1-theta Fraction',
[76567bb]462                      'Calc. Time [sec]']
[effdd98]463
[4fbf0db]464        if data is None:
465            return
[effdd98]466        keys = data.keys()
467        rows = len(keys)
468        columns = len(param_list)
469        self.tblParams.setColumnCount(columns)
470        self.tblParams.setRowCount(rows)
471
472        for i, param in enumerate(param_list):
473            self.tblParams.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
474
475        # first - Chi2 and data filename
476        for i_row, (filename, pr) in enumerate(data.items()):
477            out = pr.out
478            cov = pr.cov
[76567bb]479            if out is None:
480                logging.warning("P(r) for {} did not converge.".format(filename))
481                continue
[effdd98]482            self.tblParams.setItem(i_row, 0, QtWidgets.QTableWidgetItem(
[76567bb]483                "{}".format(filename)))
484            self.tblParams.setItem(i_row, 1, QtWidgets.QTableWidgetItem(
485                "{:.3g}".format(pr.rg(out))))
[5a5e371]486            self.tblParams.setItem(i_row, 2, QtWidgets.QTableWidgetItem(
487                "{:.3g}".format(pr.chi2[0])))
[76567bb]488            self.tblParams.setItem(i_row, 3, QtWidgets.QTableWidgetItem(
489                "{:.3g}".format(pr.iq0(out))))
490            self.tblParams.setItem(i_row, 4, QtWidgets.QTableWidgetItem(
491                "{:.3g}".format(pr.oscillations(out))))
[5a5e371]492            self.tblParams.setItem(i_row, 5, QtWidgets.QTableWidgetItem(
493                "{:.3g}".format(pr.background)))
[76567bb]494            self.tblParams.setItem(i_row, 6, QtWidgets.QTableWidgetItem(
495                "{:.3g}".format(pr.get_positive(out))))
496            self.tblParams.setItem(i_row, 7, QtWidgets.QTableWidgetItem(
497                "{:.3g}".format(pr.get_pos_err(out, cov))))
[5a5e371]498            self.tblParams.setItem(i_row, 8, QtWidgets.QTableWidgetItem(
499                "{:.2g}".format(pr.elapsed)))
[effdd98]500
501        self.tblParams.resizeColumnsToContents()
[76567bb]502
503    @classmethod
504    def onHelp(cls):
505        """
506        Open a local url in the default browser
507        """
508        location = GuiUtils.HELP_DIRECTORY_LOCATION
[75906a1]509        url = "/user/qtgui/Perspectives/Fitting/fitting_help.html#batch-fit-mode"
[76567bb]510        try:
511            webbrowser.open('file://' + os.path.realpath(location + url))
512        except webbrowser.Error as ex:
513            logging.warning("Cannot display help. %s" % ex)
[98485fe]514
515    def closeEvent(self, event):
516        """Tell the parent window the window closed"""
[ae34d30]517        self.parent.batchResultsWindow = None
[98485fe]518        event.accept()
Note: See TracBrowser for help on using the repository browser.