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

ESS_GUI_batch_fitting
Last change on this file since 9d23e4c was 9d23e4c, checked in by wojciech, 10 months ago

Fixing tables Plot activation/deactivation for batch fitting

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