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

Last change on this file since 0101c9f was 3c6ecd9, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Merge branch 'ESS_GUI' into ESS_GUI_Pr

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