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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since a4b9b7a was a4b9b7a, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Modified batch grid viewer after changes to magnetic param names.
Adjusted unit tests.

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