source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 50bfab0

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 50bfab0 was 0261bc1, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Added unit tests for recent functionality

  • Property mode set to 100644
File size: 72.6 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10
11from PyQt5 import QtCore
12from PyQt5 import QtGui
13from PyQt5 import QtWidgets
14from PyQt5 import QtWebKitWidgets
15# Officially QtWebEngineWidgets are the way to display HTML in Qt5,
16# but this module isn't ported to PyQt5 yet...
17# let's wait. In the meantime no Help.
18#from PyQt5 import QtWebEngineWidgets
19
20from sasmodels import product
21from sasmodels import generate
22from sasmodels import modelinfo
23from sasmodels.sasview_model import load_standard_models
24from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
25
26from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
27
28import sas.qtgui.Utilities.GuiUtils as GuiUtils
29from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
30from sas.qtgui.Plotting.PlotterData import Data1D
31from sas.qtgui.Plotting.PlotterData import Data2D
32
33from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
34from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
35from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
36
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
38from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
39from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
40from sas.qtgui.Perspectives.Fitting import FittingUtilities
41from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
42from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
43from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
44from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
45from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
46from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
47
48
49TAB_MAGNETISM = 4
50TAB_POLY = 3
51CATEGORY_DEFAULT = "Choose category..."
52CATEGORY_STRUCTURE = "Structure Factor"
53STRUCTURE_DEFAULT = "None"
54
55DEFAULT_POLYDISP_FUNCTION = 'gaussian'
56
57USING_TWISTED = True
58#USING_TWISTED = False
59
60class ToolTippedItemModel(QtGui.QStandardItemModel):
61    """
62    Subclass from QStandardItemModel to allow displaying tooltips in
63    QTableView model.
64    """
65    def __init__(self, parent=None):
66        QtGui.QStandardItemModel.__init__(self,parent)
67
68    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
69        """
70        Displays tooltip for each column's header
71        :param section:
72        :param orientation:
73        :param role:
74        :return:
75        """
76        if role == QtCore.Qt.ToolTipRole:
77            if orientation == QtCore.Qt.Horizontal:
78                return str(self.header_tooltips[section])
79
80        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
81
82class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
83    """
84    Main widget for selecting form and structure factor models
85    """
86    def __init__(self, parent=None, data=None, tab_id=1):
87
88        super(FittingWidget, self).__init__()
89
90        # Necessary globals
91        self.parent = parent
92
93        # Which tab is this widget displayed in?
94        self.tab_id = tab_id
95
96        # Main Data[12]D holder
97        self.logic = FittingLogic()
98
99        # Globals
100        self.initializeGlobals()
101
102        # Main GUI setup up
103        self.setupUi(self)
104        self.setWindowTitle("Fitting")
105
106        # Set up tabs widgets
107        self.initializeWidgets()
108
109        # Set up models and views
110        self.initializeModels()
111
112        # Defaults for the structure factors
113        self.setDefaultStructureCombo()
114
115        # Make structure factor and model CBs disabled
116        self.disableModelCombo()
117        self.disableStructureCombo()
118
119        # Generate the category list for display
120        self.initializeCategoryCombo()
121
122        # Connect signals to controls
123        self.initializeSignals()
124
125        # Initial control state
126        self.initializeControls()
127
128        # Display HTML content
129        self.helpView = QtWebKitWidgets.QWebView()
130
131        # New font to display angstrom symbol
132        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
133        self.label_17.setStyleSheet(new_font)
134        self.label_19.setStyleSheet(new_font)
135
136        self._index = None
137        if data is not None:
138            self.data = data
139
140    @property
141    def data(self):
142        return self.logic.data
143
144    @data.setter
145    def data(self, value):
146        """ data setter """
147        # Value is either a list of indices for batch fitting or a simple index
148        # for standard fitting. Assure we have a list, regardless.
149        if isinstance(value, list):
150            self.is_batch_fitting = True
151        else:
152            value = [value]
153
154        assert isinstance(value[0], QtGui.QStandardItem)
155        # _index contains the QIndex with data
156        self._index = value[0]
157
158        # Keep reference to all datasets for batch
159        self.all_data = value
160
161        # Update logics with data items
162        # Logics.data contains only a single Data1D/Data2D object
163        self.logic.data = GuiUtils.dataFromItem(value[0])
164
165        # Overwrite data type descriptor
166        self.is2D = True if isinstance(self.logic.data, Data2D) else False
167
168        # Let others know we're full of data now
169        self.data_is_loaded = True
170
171        # Enable/disable UI components
172        self.setEnablementOnDataLoad()
173
174    def initializeGlobals(self):
175        """
176        Initialize global variables used in this class
177        """
178        # SasModel is loaded
179        self.model_is_loaded = False
180        # Data[12]D passed and set
181        self.data_is_loaded = False
182        # Batch/single fitting
183        self.is_batch_fitting = False
184        # Current SasModel in view
185        self.kernel_module = None
186        # Current SasModel view dimension
187        self.is2D = False
188        # Current SasModel is multishell
189        self.model_has_shells = False
190        # Utility variable to enable unselectable option in category combobox
191        self._previous_category_index = 0
192        # Utility variable for multishell display
193        self._last_model_row = 0
194        # Dictionary of {model name: model class} for the current category
195        self.models = {}
196        # Parameters to fit
197        self.parameters_to_fit = None
198        # Fit options
199        self.q_range_min = 0.005
200        self.q_range_max = 0.1
201        self.npts = 25
202        self.log_points = False
203        self.weighting = 0
204        self.chi2 = None
205        # Does the control support UNDO/REDO
206        # temporarily off
207        self.undo_supported = False
208        self.page_stack = []
209        self.all_data = []
210        # Polydisp widget table default index for function combobox
211        self.orig_poly_index = 3
212
213        # Data for chosen model
214        self.model_data = None
215
216        # Which shell is being currently displayed?
217        self.current_shell_displayed = 0
218        # List of all shell-unique parameters
219        self.shell_names = []
220
221        # Error column presence in parameter display
222        self.has_error_column = False
223        self.has_poly_error_column = False
224        self.has_magnet_error_column = False
225
226        # signal communicator
227        self.communicate = self.parent.communicate
228
229    def initializeWidgets(self):
230        """
231        Initialize widgets for tabs
232        """
233        # Options widget
234        layout = QtWidgets.QGridLayout()
235        self.options_widget = OptionsWidget(self, self.logic)
236        layout.addWidget(self.options_widget)
237        self.tabOptions.setLayout(layout)
238
239        # Smearing widget
240        layout = QtWidgets.QGridLayout()
241        self.smearing_widget = SmearingWidget(self)
242        layout.addWidget(self.smearing_widget)
243        self.tabResolution.setLayout(layout)
244
245        # Define bold font for use in various controls
246        self.boldFont = QtGui.QFont()
247        self.boldFont.setBold(True)
248
249        # Set data label
250        self.label.setFont(self.boldFont)
251        self.label.setText("No data loaded")
252        self.lblFilename.setText("")
253
254        # Magnetic angles explained in one picture
255        self.magneticAnglesWidget = QtWidgets.QWidget()
256        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
257        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.bmp')
258        labl.setPixmap(pixmap)
259        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
260
261    def initializeModels(self):
262        """
263        Set up models and views
264        """
265        # Set the main models
266        # We can't use a single model here, due to restrictions on flattening
267        # the model tree with subclassed QAbstractProxyModel...
268        self._model_model = ToolTippedItemModel()
269        self._poly_model = ToolTippedItemModel()
270        self._magnet_model = ToolTippedItemModel()
271
272        # Param model displayed in param list
273        self.lstParams.setModel(self._model_model)
274        self.readCategoryInfo()
275
276        self.model_parameters = None
277
278        # Delegates for custom editing and display
279        self.lstParams.setItemDelegate(ModelViewDelegate(self))
280
281        self.lstParams.setAlternatingRowColors(True)
282        stylesheet = """
283
284            QTreeView {
285                paint-alternating-row-colors-for-empty-area:0;
286            }
287
288            QTreeView::item:hover {
289                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
290                border: 1px solid #bfcde4;
291            }
292
293            QTreeView::item:selected {
294                border: 1px solid #567dbc;
295            }
296
297            QTreeView::item:selected:active{
298                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
299            }
300
301            QTreeView::item:selected:!active {
302                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
303            }
304           """
305        self.lstParams.setStyleSheet(stylesheet)
306        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
307        self.lstParams.customContextMenuRequested.connect(self.showModelDescription)
308        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
309        # Poly model displayed in poly list
310        self.lstPoly.setModel(self._poly_model)
311        self.setPolyModel()
312        self.setTableProperties(self.lstPoly)
313        # Delegates for custom editing and display
314        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
315        # Polydispersity function combo response
316        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
317        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
318
319        # Magnetism model displayed in magnetism list
320        self.lstMagnetic.setModel(self._magnet_model)
321        self.setMagneticModel()
322        self.setTableProperties(self.lstMagnetic)
323        # Delegates for custom editing and display
324        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
325
326    def initializeCategoryCombo(self):
327        """
328        Model category combo setup
329        """
330        category_list = sorted(self.master_category_dict.keys())
331        self.cbCategory.addItem(CATEGORY_DEFAULT)
332        self.cbCategory.addItems(category_list)
333        self.cbCategory.addItem(CATEGORY_STRUCTURE)
334        self.cbCategory.setCurrentIndex(0)
335
336    def setEnablementOnDataLoad(self):
337        """
338        Enable/disable various UI elements based on data loaded
339        """
340        # Tag along functionality
341        self.label.setText("Data loaded from: ")
342        self.lblFilename.setText(self.logic.data.filename)
343        self.updateQRange()
344        # Switch off Data2D control
345        self.chk2DView.setEnabled(False)
346        self.chk2DView.setVisible(False)
347        self.chkMagnetism.setEnabled(self.is2D)
348        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.is2D)
349        # Combo box or label for file name"
350        if self.is_batch_fitting:
351            self.lblFilename.setVisible(False)
352            for dataitem in self.all_data:
353                filename = GuiUtils.dataFromItem(dataitem).filename
354                self.cbFileNames.addItem(filename)
355            self.cbFileNames.setVisible(True)
356            # This panel is not designed to view individual fits, so disable plotting
357            self.cmdPlot.setVisible(False)
358        # Similarly on other tabs
359        self.options_widget.setEnablementOnDataLoad()
360        self.onSelectModel()
361        # Smearing tab
362        self.smearing_widget.updateSmearing(self.data)
363
364    def acceptsData(self):
365        """ Tells the caller this widget can accept new dataset """
366        return not self.data_is_loaded
367
368    def disableModelCombo(self):
369        """ Disable the combobox """
370        self.cbModel.setEnabled(False)
371        self.lblModel.setEnabled(False)
372
373    def enableModelCombo(self):
374        """ Enable the combobox """
375        self.cbModel.setEnabled(True)
376        self.lblModel.setEnabled(True)
377
378    def disableStructureCombo(self):
379        """ Disable the combobox """
380        self.cbStructureFactor.setEnabled(False)
381        self.lblStructure.setEnabled(False)
382
383    def enableStructureCombo(self):
384        """ Enable the combobox """
385        self.cbStructureFactor.setEnabled(True)
386        self.lblStructure.setEnabled(True)
387
388    def togglePoly(self, isChecked):
389        """ Enable/disable the polydispersity tab """
390        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
391
392    def toggleMagnetism(self, isChecked):
393        """ Enable/disable the magnetism tab """
394        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
395
396    def toggle2D(self, isChecked):
397        """ Enable/disable the controls dependent on 1D/2D data instance """
398        self.chkMagnetism.setEnabled(isChecked)
399        self.is2D = isChecked
400        # Reload the current model
401        if self.kernel_module:
402            self.onSelectModel()
403
404    def initializeControls(self):
405        """
406        Set initial control enablement
407        """
408        self.cbFileNames.setVisible(False)
409        self.cmdFit.setEnabled(False)
410        self.cmdPlot.setEnabled(False)
411        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
412        self.chkPolydispersity.setEnabled(True)
413        self.chkPolydispersity.setCheckState(False)
414        self.chk2DView.setEnabled(True)
415        self.chk2DView.setCheckState(False)
416        self.chkMagnetism.setEnabled(False)
417        self.chkMagnetism.setCheckState(False)
418        # Tabs
419        self.tabFitting.setTabEnabled(TAB_POLY, False)
420        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
421        self.lblChi2Value.setText("---")
422        # Smearing tab
423        self.smearing_widget.updateSmearing(self.data)
424        # Line edits in the option tab
425        self.updateQRange()
426
427    def initializeSignals(self):
428        """
429        Connect GUI element signals
430        """
431        # Comboboxes
432        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
433        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
434        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
435        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
436        # Checkboxes
437        self.chk2DView.toggled.connect(self.toggle2D)
438        self.chkPolydispersity.toggled.connect(self.togglePoly)
439        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
440        # Buttons
441        self.cmdFit.clicked.connect(self.onFit)
442        self.cmdPlot.clicked.connect(self.onPlot)
443        self.cmdHelp.clicked.connect(self.onHelp)
444        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
445
446        # Respond to change in parameters from the UI
447        self._model_model.itemChanged.connect(self.onMainParamsChange)
448        self._poly_model.itemChanged.connect(self.onPolyModelChange)
449        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
450
451        # Signals from separate tabs asking for replot
452        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
453
454    def showModelDescription(self, position):
455        """
456        Shows a window with model description, when right clicked in the treeview
457        """
458        msg = 'Model description:\n'
459        if self.kernel_module is not None:
460            if str(self.kernel_module.description).rstrip().lstrip() == '':
461                msg += "Sorry, no information is available for this model."
462            else:
463                msg += self.kernel_module.description + '\n'
464        else:
465            msg += "You must select a model to get information on this"
466
467        menu = QtWidgets.QMenu()
468        label = QtWidgets.QLabel(msg)
469        action = QtWidgets.QWidgetAction(self)
470        action.setDefaultWidget(label)
471        menu.addAction(action)
472        menu.exec_(self.lstParams.viewport().mapToGlobal(position))
473
474    def onSelectModel(self):
475        """
476        Respond to select Model from list event
477        """
478        model = self.cbModel.currentText()
479
480        # empty combobox forced to be read
481        if not model:
482            return
483        # Reset structure factor
484        self.cbStructureFactor.setCurrentIndex(0)
485
486        # Reset parameters to fit
487        self.parameters_to_fit = None
488        self.has_error_column = False
489        self.has_poly_error_column = False
490
491        self.respondToModelStructure(model=model, structure_factor=None)
492
493    def onSelectBatchFilename(self, data_index):
494        """
495        Update the logic based on the selected file in batch fitting
496        """
497        self._index = self.all_data[data_index]
498        self.logic.data = GuiUtils.dataFromItem(self.all_data[data_index])
499        self.updateQRange()
500
501    def onSelectStructureFactor(self):
502        """
503        Select Structure Factor from list
504        """
505        model = str(self.cbModel.currentText())
506        category = str(self.cbCategory.currentText())
507        structure = str(self.cbStructureFactor.currentText())
508        if category == CATEGORY_STRUCTURE:
509            model = None
510        self.respondToModelStructure(model=model, structure_factor=structure)
511
512    def respondToModelStructure(self, model=None, structure_factor=None):
513        # Set enablement on calculate/plot
514        self.cmdPlot.setEnabled(True)
515
516        # kernel parameters -> model_model
517        self.SASModelToQModel(model, structure_factor)
518
519        if self.data_is_loaded:
520            self.cmdPlot.setText("Show Plot")
521            self.calculateQGridForModel()
522        else:
523            self.cmdPlot.setText("Calculate")
524            # Create default datasets if no data passed
525            self.createDefaultDataset()
526
527        # Update state stack
528        self.updateUndo()
529
530    def onSelectCategory(self):
531        """
532        Select Category from list
533        """
534        category = self.cbCategory.currentText()
535        # Check if the user chose "Choose category entry"
536        if category == CATEGORY_DEFAULT:
537            # if the previous category was not the default, keep it.
538            # Otherwise, just return
539            if self._previous_category_index != 0:
540                # We need to block signals, or else state changes on perceived unchanged conditions
541                self.cbCategory.blockSignals(True)
542                self.cbCategory.setCurrentIndex(self._previous_category_index)
543                self.cbCategory.blockSignals(False)
544            return
545
546        if category == CATEGORY_STRUCTURE:
547            self.disableModelCombo()
548            self.enableStructureCombo()
549            self._model_model.clear()
550            return
551
552        # Safely clear and enable the model combo
553        self.cbModel.blockSignals(True)
554        self.cbModel.clear()
555        self.cbModel.blockSignals(False)
556        self.enableModelCombo()
557        self.disableStructureCombo()
558
559        self._previous_category_index = self.cbCategory.currentIndex()
560        # Retrieve the list of models
561        model_list = self.master_category_dict[category]
562        # Populate the models combobox
563        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
564
565    def onPolyModelChange(self, item):
566        """
567        Callback method for updating the main model and sasmodel
568        parameters with the GUI values in the polydispersity view
569        """
570        model_column = item.column()
571        model_row = item.row()
572        name_index = self._poly_model.index(model_row, 0)
573        parameter_name = str(name_index.data()).lower() # "distribution of sld" etc.
574        if "distribution of" in parameter_name:
575            # just the last word
576            parameter_name = parameter_name.rsplit()[-1]
577
578        # Extract changed value.
579        if model_column == self.lstPoly.itemDelegate().poly_parameter:
580            # Is the parameter checked for fitting?
581            value = item.checkState()
582            parameter_name = parameter_name + '.width'
583            if value == QtCore.Qt.Checked:
584                self.parameters_to_fit.append(parameter_name)
585            else:
586                if parameter_name in self.parameters_to_fit:
587                    self.parameters_to_fit.remove(parameter_name)
588            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
589            return
590        elif model_column in [self.lstPoly.itemDelegate().poly_min, self.lstPoly.itemDelegate().poly_max]:
591            try:
592                value = GuiUtils.toDouble(item.text())
593            except TypeError:
594                # Can't be converted properly, bring back the old value and exit
595                return
596
597            current_details = self.kernel_module.details[parameter_name]
598            current_details[model_column-1] = value
599        elif model_column == self.lstPoly.itemDelegate().poly_function:
600            # name of the function - just pass
601            return
602        elif model_column == self.lstPoly.itemDelegate().poly_filename:
603            # filename for array - just pass
604            return
605        else:
606            try:
607                value = GuiUtils.toDouble(item.text())
608            except TypeError:
609                # Can't be converted properly, bring back the old value and exit
610                return
611
612            # Update the sasmodel
613            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
614            self.kernel_module.setParam(parameter_name + '.' + \
615                                        self.lstPoly.itemDelegate().columnDict()[model_column], value)
616
617    def onMagnetModelChange(self, item):
618        """
619        Callback method for updating the sasmodel magnetic parameters with the GUI values
620        """
621        model_column = item.column()
622        model_row = item.row()
623        name_index = self._magnet_model.index(model_row, 0)
624        parameter_name = str(self._magnet_model.data(name_index))
625
626        if model_column == 0:
627            value = item.checkState()
628            if value == QtCore.Qt.Checked:
629                self.parameters_to_fit.append(parameter_name)
630            else:
631                if parameter_name in self.parameters_to_fit:
632                    self.parameters_to_fit.remove(parameter_name)
633            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
634            # Update state stack
635            self.updateUndo()
636            return
637
638        # Extract changed value.
639        try:
640            value = GuiUtils.toDouble(item.text())
641        except TypeError:
642            # Unparsable field
643            return
644
645        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
646
647        # Update the parameter value - note: this supports +/-inf as well
648        self.kernel_module.params[parameter_name] = value
649
650        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
651        self.kernel_module.details[parameter_name][property_index] = value
652
653        # Force the chart update when actual parameters changed
654        if model_column == 1:
655            self.recalculatePlotData()
656
657        # Update state stack
658        self.updateUndo()
659
660    def onHelp(self):
661        """
662        Show the "Fitting" section of help
663        """
664        tree_location = GuiUtils.HELP_DIRECTORY_LOCATION + "/user/sasgui/perspectives/fitting/"
665
666        # Actual file will depend on the current tab
667        tab_id = self.tabFitting.currentIndex()
668        helpfile = "fitting.html"
669        if tab_id == 0:
670            helpfile = "fitting_help.html"
671        elif tab_id == 1:
672            helpfile = "residuals_help.html"
673        elif tab_id == 2:
674            helpfile = "sm_help.html"
675        elif tab_id == 3:
676            helpfile = "pd_help.html"
677        elif tab_id == 4:
678            helpfile = "mag_help.html"
679        help_location = tree_location + helpfile
680
681        content = QtCore.QUrl(help_location)
682        self.helpView.load(QtCore.QUrl(help_location))
683        self.helpView.show()
684
685    def onDisplayMagneticAngles(self):
686        """
687        Display a simple image showing direction of magnetic angles
688        """
689        self.magneticAnglesWidget.show()
690
691    def onFit(self):
692        """
693        Perform fitting on the current data
694        """
695
696        # Data going in
697        data = self.logic.data
698        model = self.kernel_module
699        qmin = self.q_range_min
700        qmax = self.q_range_max
701        params_to_fit = self.parameters_to_fit
702
703        # Potential weights added directly to data
704        self.addWeightingToData(data)
705
706        # Potential smearing added
707        # Remember that smearing_min/max can be None ->
708        # deal with it until Python gets discriminated unions
709        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
710
711        # These should be updating somehow?
712        fit_id = 0
713        constraints = []
714        smearer = None
715        page_id = [210]
716        handler = None
717        batch_inputs = {}
718        batch_outputs = {}
719        list_page_id = [page_id]
720        #---------------------------------
721        if USING_TWISTED:
722            handler = None
723            updater = None
724        else:
725            handler = ConsoleUpdate(parent=self.parent,
726                                    manager=self,
727                                    improvement_delta=0.1)
728            updater = handler.update_fit
729
730        # Parameterize the fitter
731        fitters = []
732        for fit_index in self.all_data:
733            fitter = Fit()
734            data = GuiUtils.dataFromItem(fit_index)
735            try:
736                fitter.set_model(model, fit_id, params_to_fit, data=data,
737                             constraints=constraints)
738            except ValueError as ex:
739                logging.error("Setting model parameters failed with: %s" % ex)
740                return
741
742            qmin, qmax, _ = self.logic.computeRangeFromData(data)
743            fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
744                            qmax=qmax)
745            fitter.select_problem_for_fit(id=fit_id, value=1)
746            fitter.fitter_id = page_id
747            fit_id += 1
748            fitters.append(fitter)
749
750        # Create the fitting thread, based on the fitter
751        completefn = self.batchFitComplete if self.is_batch_fitting else self.fitComplete
752
753        calc_fit = FitThread(handler=handler,
754                                fn=fitters,
755                                batch_inputs=batch_inputs,
756                                batch_outputs=batch_outputs,
757                                page_id=list_page_id,
758                                updatefn=updater,
759                                completefn=completefn)
760
761        if USING_TWISTED:
762            # start the trhrhread with twisted
763            calc_thread = threads.deferToThread(calc_fit.compute)
764            calc_thread.addCallback(self.fitComplete)
765            calc_thread.addErrback(self.fitFailed)
766        else:
767            # Use the old python threads + Queue
768            calc_fit.queue()
769            calc_fit.ready(2.5)
770
771
772        #disable the Fit button
773        self.cmdFit.setText('Running...')
774        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
775        self.cmdFit.setEnabled(False)
776
777    def updateFit(self):
778        """
779        """
780        print("UPDATE FIT")
781        pass
782
783    def fitFailed(self, reason):
784        """
785        """
786        print("FIT FAILED: ", reason)
787        pass
788
789    def batchFitComplete(self, result):
790        """
791        Receive and display batch fitting results
792        """
793        #re-enable the Fit button
794        self.cmdFit.setText("Fit")
795        self.cmdFit.setEnabled(True)
796
797        print ("BATCH FITTING FINISHED")
798        # Add the Qt version of wx.aui.AuiNotebook and populate it
799        pass
800
801    def fitComplete(self, result):
802        """
803        Receive and display fitting results
804        "result" is a tuple of actual result list and the fit time in seconds
805        """
806        #re-enable the Fit button
807        self.cmdFit.setText("Fit")
808        self.cmdFit.setEnabled(True)
809
810        assert result is not None
811
812        res_list = result[0][0]
813        res = res_list[0]
814        if res.fitness is None or \
815            not np.isfinite(res.fitness) or \
816            np.any(res.pvec is None) or \
817            not np.all(np.isfinite(res.pvec)):
818            msg = "Fitting did not converge!!!"
819            self.communicate.statusBarUpdateSignal.emit(msg)
820            logging.error(msg)
821            return
822
823        elapsed = result[1]
824        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
825        self.communicate.statusBarUpdateSignal.emit(msg)
826
827        self.chi2 = res.fitness
828        param_list = res.param_list # ['radius', 'radius.width']
829        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
830        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
831        params_and_errors = list(zip(param_values, param_stderr))
832        param_dict = dict(zip(param_list, params_and_errors))
833
834        # Dictionary of fitted parameter: value, error
835        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
836        self.updateModelFromList(param_dict)
837
838        self.updatePolyModelFromList(param_dict)
839
840        self.updateMagnetModelFromList(param_dict)
841
842        # update charts
843        self.onPlot()
844
845        # Read only value - we can get away by just printing it here
846        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
847        self.lblChi2Value.setText(chi2_repr)
848
849    def iterateOverModel(self, func):
850        """
851        Take func and throw it inside the model row loop
852        """
853        for row_i in range(self._model_model.rowCount()):
854            func(row_i)
855
856    def updateModelFromList(self, param_dict):
857        """
858        Update the model with new parameters, create the errors column
859        """
860        assert isinstance(param_dict, dict)
861        if not dict:
862            return
863
864        def updateFittedValues(row):
865            # Utility function for main model update
866            # internal so can use closure for param_dict
867            param_name = str(self._model_model.item(row, 0).text())
868            if param_name not in list(param_dict.keys()):
869                return
870            # modify the param value
871            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
872            self._model_model.item(row, 1).setText(param_repr)
873            if self.has_error_column:
874                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
875                self._model_model.item(row, 2).setText(error_repr)
876
877        def updatePolyValues(row):
878            # Utility function for updateof polydispersity part of the main model
879            param_name = str(self._model_model.item(row, 0).text())+'.width'
880            if param_name not in list(param_dict.keys()):
881                return
882            # modify the param value
883            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
884            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
885
886        def createErrorColumn(row):
887            # Utility function for error column update
888            item = QtGui.QStandardItem()
889            def createItem(param_name):
890                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
891                item.setText(error_repr)
892            def curr_param():
893                return str(self._model_model.item(row, 0).text())
894
895            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
896
897            error_column.append(item)
898
899        # block signals temporarily, so we don't end up
900        # updating charts with every single model change on the end of fitting
901        self._model_model.blockSignals(True)
902        self.iterateOverModel(updateFittedValues)
903        self.iterateOverModel(updatePolyValues)
904        self._model_model.blockSignals(False)
905
906        if self.has_error_column:
907            return
908
909        error_column = []
910        self.lstParams.itemDelegate().addErrorColumn()
911        self.iterateOverModel(createErrorColumn)
912
913        # switch off reponse to model change
914        self._model_model.blockSignals(True)
915        self._model_model.insertColumn(2, error_column)
916        self._model_model.blockSignals(False)
917        FittingUtilities.addErrorHeadersToModel(self._model_model)
918        # Adjust the table cells width.
919        # TODO: find a way to dynamically adjust column width while resized expanding
920        self.lstParams.resizeColumnToContents(0)
921        self.lstParams.resizeColumnToContents(4)
922        self.lstParams.resizeColumnToContents(5)
923        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
924
925        self.has_error_column = True
926
927    def updatePolyModelFromList(self, param_dict):
928        """
929        Update the polydispersity model with new parameters, create the errors column
930        """
931        assert isinstance(param_dict, dict)
932        if not dict:
933            return
934
935        def iterateOverPolyModel(func):
936            """
937            Take func and throw it inside the poly model row loop
938            """
939            for row_i in range(self._poly_model.rowCount()):
940                func(row_i)
941
942        def updateFittedValues(row_i):
943            # Utility function for main model update
944            # internal so can use closure for param_dict
945            if row_i >= self._poly_model.rowCount():
946                return
947            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
948            if param_name not in list(param_dict.keys()):
949                return
950            # modify the param value
951            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
952            self._poly_model.item(row_i, 1).setText(param_repr)
953            if self.has_poly_error_column:
954                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
955                self._poly_model.item(row_i, 2).setText(error_repr)
956
957
958        def createErrorColumn(row_i):
959            # Utility function for error column update
960            if row_i >= self._poly_model.rowCount():
961                return
962            item = QtGui.QStandardItem()
963
964            def createItem(param_name):
965                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
966                item.setText(error_repr)
967
968            def poly_param():
969                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
970
971            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
972
973            error_column.append(item)
974
975        # block signals temporarily, so we don't end up
976        # updating charts with every single model change on the end of fitting
977        self._poly_model.blockSignals(True)
978        iterateOverPolyModel(updateFittedValues)
979        self._poly_model.blockSignals(False)
980
981        if self.has_poly_error_column:
982            return
983
984        self.lstPoly.itemDelegate().addErrorColumn()
985        error_column = []
986        iterateOverPolyModel(createErrorColumn)
987
988        # switch off reponse to model change
989        self._poly_model.blockSignals(True)
990        self._poly_model.insertColumn(2, error_column)
991        self._poly_model.blockSignals(False)
992        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
993
994        self.has_poly_error_column = True
995
996    def updateMagnetModelFromList(self, param_dict):
997        """
998        Update the magnetic model with new parameters, create the errors column
999        """
1000        assert isinstance(param_dict, dict)
1001        if not dict:
1002            return
1003        if self._model_model.rowCount() == 0:
1004            return
1005
1006        def iterateOverMagnetModel(func):
1007            """
1008            Take func and throw it inside the magnet model row loop
1009            """
1010            for row_i in range(self._model_model.rowCount()):
1011                func(row_i)
1012
1013        def updateFittedValues(row):
1014            # Utility function for main model update
1015            # internal so can use closure for param_dict
1016            if self._magnet_model.item(row, 0) is None:
1017                return
1018            param_name = str(self._magnet_model.item(row, 0).text())
1019            if param_name not in list(param_dict.keys()):
1020                return
1021            # modify the param value
1022            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1023            self._magnet_model.item(row, 1).setText(param_repr)
1024            if self.has_magnet_error_column:
1025                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1026                self._magnet_model.item(row, 2).setText(error_repr)
1027
1028        def createErrorColumn(row):
1029            # Utility function for error column update
1030            item = QtGui.QStandardItem()
1031            def createItem(param_name):
1032                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1033                item.setText(error_repr)
1034            def curr_param():
1035                return str(self._magnet_model.item(row, 0).text())
1036
1037            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1038
1039            error_column.append(item)
1040
1041        # block signals temporarily, so we don't end up
1042        # updating charts with every single model change on the end of fitting
1043        self._magnet_model.blockSignals(True)
1044        iterateOverMagnetModel(updateFittedValues)
1045        self._magnet_model.blockSignals(False)
1046
1047        if self.has_magnet_error_column:
1048            return
1049
1050        self.lstMagnetic.itemDelegate().addErrorColumn()
1051        error_column = []
1052        iterateOverMagnetModel(createErrorColumn)
1053
1054        # switch off reponse to model change
1055        self._magnet_model.blockSignals(True)
1056        self._magnet_model.insertColumn(2, error_column)
1057        self._magnet_model.blockSignals(False)
1058        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1059
1060        self.has_magnet_error_column = True
1061
1062    def onPlot(self):
1063        """
1064        Plot the current set of data
1065        """
1066        # Regardless of previous state, this should now be `plot show` functionality only
1067        self.cmdPlot.setText("Show Plot")
1068        # Force data recalculation so existing charts are updated
1069        self.recalculatePlotData()
1070        self.showPlot()
1071
1072    def recalculatePlotData(self):
1073        """
1074        Generate a new dataset for model
1075        """
1076        if not self.data_is_loaded:
1077            self.createDefaultDataset()
1078        self.calculateQGridForModel()
1079
1080    def showPlot(self):
1081        """
1082        Show the current plot in MPL
1083        """
1084        # Show the chart if ready
1085        data_to_show = self.data if self.data_is_loaded else self.model_data
1086        if data_to_show is not None:
1087            self.communicate.plotRequestedSignal.emit([data_to_show])
1088
1089    def onOptionsUpdate(self):
1090        """
1091        Update local option values and replot
1092        """
1093        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1094            self.options_widget.state()
1095        # set Q range labels on the main tab
1096        self.lblMinRangeDef.setText(str(self.q_range_min))
1097        self.lblMaxRangeDef.setText(str(self.q_range_max))
1098        self.recalculatePlotData()
1099
1100    def setDefaultStructureCombo(self):
1101        """
1102        Fill in the structure factors combo box with defaults
1103        """
1104        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1105        factors = [factor[0] for factor in structure_factor_list]
1106        factors.insert(0, STRUCTURE_DEFAULT)
1107        self.cbStructureFactor.clear()
1108        self.cbStructureFactor.addItems(sorted(factors))
1109
1110    def createDefaultDataset(self):
1111        """
1112        Generate default Dataset 1D/2D for the given model
1113        """
1114        # Create default datasets if no data passed
1115        if self.is2D:
1116            qmax = self.q_range_max/np.sqrt(2)
1117            qstep = self.npts
1118            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1119            return
1120        elif self.log_points:
1121            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1122            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1123            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1124        else:
1125            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1126                                   num=self.npts, endpoint=True)
1127        self.logic.createDefault1dData(interval, self.tab_id)
1128
1129    def readCategoryInfo(self):
1130        """
1131        Reads the categories in from file
1132        """
1133        self.master_category_dict = defaultdict(list)
1134        self.by_model_dict = defaultdict(list)
1135        self.model_enabled_dict = defaultdict(bool)
1136
1137        categorization_file = CategoryInstaller.get_user_file()
1138        if not os.path.isfile(categorization_file):
1139            categorization_file = CategoryInstaller.get_default_file()
1140        with open(categorization_file, 'rb') as cat_file:
1141            self.master_category_dict = json.load(cat_file)
1142            self.regenerateModelDict()
1143
1144        # Load the model dict
1145        models = load_standard_models()
1146        for model in models:
1147            self.models[model.name] = model
1148
1149    def regenerateModelDict(self):
1150        """
1151        Regenerates self.by_model_dict which has each model name as the
1152        key and the list of categories belonging to that model
1153        along with the enabled mapping
1154        """
1155        self.by_model_dict = defaultdict(list)
1156        for category in self.master_category_dict:
1157            for (model, enabled) in self.master_category_dict[category]:
1158                self.by_model_dict[model].append(category)
1159                self.model_enabled_dict[model] = enabled
1160
1161    def addBackgroundToModel(self, model):
1162        """
1163        Adds background parameter with default values to the model
1164        """
1165        assert isinstance(model, QtGui.QStandardItemModel)
1166        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1167        FittingUtilities.addCheckedListToModel(model, checked_list)
1168        last_row = model.rowCount()-1
1169        model.item(last_row, 0).setEditable(False)
1170        model.item(last_row, 4).setEditable(False)
1171
1172    def addScaleToModel(self, model):
1173        """
1174        Adds scale parameter with default values to the model
1175        """
1176        assert isinstance(model, QtGui.QStandardItemModel)
1177        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1178        FittingUtilities.addCheckedListToModel(model, checked_list)
1179        last_row = model.rowCount()-1
1180        model.item(last_row, 0).setEditable(False)
1181        model.item(last_row, 4).setEditable(False)
1182
1183    def addWeightingToData(self, data):
1184        """
1185        Adds weighting contribution to fitting data
1186        """
1187        # Send original data for weighting
1188        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1189        update_module = data.err_data if self.is2D else data.dy
1190        # Overwrite relevant values in data
1191        update_module = weight
1192
1193    def updateQRange(self):
1194        """
1195        Updates Q Range display
1196        """
1197        if self.data_is_loaded:
1198            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1199        # set Q range labels on the main tab
1200        self.lblMinRangeDef.setText(str(self.q_range_min))
1201        self.lblMaxRangeDef.setText(str(self.q_range_max))
1202        # set Q range labels on the options tab
1203        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1204
1205    def SASModelToQModel(self, model_name, structure_factor=None):
1206        """
1207        Setting model parameters into table based on selected category
1208        """
1209        # Crete/overwrite model items
1210        self._model_model.clear()
1211
1212        # First, add parameters from the main model
1213        if model_name is not None:
1214            self.fromModelToQModel(model_name)
1215
1216        # Then, add structure factor derived parameters
1217        if structure_factor is not None and structure_factor != "None":
1218            if model_name is None:
1219                # Instantiate the current sasmodel for SF-only models
1220                self.kernel_module = self.models[structure_factor]()
1221            self.fromStructureFactorToQModel(structure_factor)
1222        else:
1223            # Allow the SF combobox visibility for the given sasmodel
1224            self.enableStructureFactorControl(structure_factor)
1225
1226        # Then, add multishells
1227        if model_name is not None:
1228            # Multishell models need additional treatment
1229            self.addExtraShells()
1230
1231        # Add polydispersity to the model
1232        self.setPolyModel()
1233        # Add magnetic parameters to the model
1234        self.setMagneticModel()
1235
1236        # Adjust the table cells width
1237        self.lstParams.resizeColumnToContents(0)
1238        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1239
1240        # Now we claim the model has been loaded
1241        self.model_is_loaded = True
1242
1243        # (Re)-create headers
1244        FittingUtilities.addHeadersToModel(self._model_model)
1245        self.lstParams.header().setFont(self.boldFont)
1246
1247        # Update Q Ranges
1248        self.updateQRange()
1249
1250    def fromModelToQModel(self, model_name):
1251        """
1252        Setting model parameters into QStandardItemModel based on selected _model_
1253        """
1254        kernel_module = generate.load_kernel_module(model_name)
1255        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1256
1257        # Instantiate the current sasmodel
1258        self.kernel_module = self.models[model_name]()
1259
1260        # Explicitly add scale and background with default values
1261        temp_undo_state = self.undo_supported
1262        self.undo_supported = False
1263        self.addScaleToModel(self._model_model)
1264        self.addBackgroundToModel(self._model_model)
1265        self.undo_supported = temp_undo_state
1266
1267        self.shell_names = self.shellNamesList()
1268
1269        # Update the QModel
1270        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1271
1272        for row in new_rows:
1273            self._model_model.appendRow(row)
1274        # Update the counter used for multishell display
1275        self._last_model_row = self._model_model.rowCount()
1276
1277    def fromStructureFactorToQModel(self, structure_factor):
1278        """
1279        Setting model parameters into QStandardItemModel based on selected _structure factor_
1280        """
1281        structure_module = generate.load_kernel_module(structure_factor)
1282        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1283        structure_kernel = self.models[structure_factor]()
1284
1285        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
1286
1287        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1288        for row in new_rows:
1289            self._model_model.appendRow(row)
1290        # Update the counter used for multishell display
1291        self._last_model_row = self._model_model.rowCount()
1292
1293    def onMainParamsChange(self, item):
1294        """
1295        Callback method for updating the sasmodel parameters with the GUI values
1296        """
1297        model_column = item.column()
1298
1299        if model_column == 0:
1300            self.checkboxSelected(item)
1301            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1302            # Update state stack
1303            self.updateUndo()
1304            return
1305
1306        model_row = item.row()
1307        name_index = self._model_model.index(model_row, 0)
1308
1309        # Extract changed value.
1310        try:
1311            value = GuiUtils.toDouble(item.text())
1312        except TypeError:
1313            # Unparsable field
1314            return
1315
1316        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
1317
1318        # Update the parameter value - note: this supports +/-inf as well
1319        self.kernel_module.params[parameter_name] = value
1320
1321        # Update the parameter value - note: this supports +/-inf as well
1322        param_column = self.lstParams.itemDelegate().param_value
1323        min_column = self.lstParams.itemDelegate().param_min
1324        max_column = self.lstParams.itemDelegate().param_max
1325        if model_column == param_column:
1326            self.kernel_module.setParam(parameter_name, value)
1327        elif model_column == min_column:
1328            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1329            self.kernel_module.details[parameter_name][1] = value
1330        elif model_column == max_column:
1331            self.kernel_module.details[parameter_name][2] = value
1332        else:
1333            # don't update the chart
1334            return
1335
1336        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1337        # TODO: multishell params in self.kernel_module.details[??] = value
1338
1339        # Force the chart update when actual parameters changed
1340        if model_column == 1:
1341            self.recalculatePlotData()
1342
1343        # Update state stack
1344        self.updateUndo()
1345
1346    def checkboxSelected(self, item):
1347        # Assure we're dealing with checkboxes
1348        if not item.isCheckable():
1349            return
1350        status = item.checkState()
1351
1352        def isCheckable(row):
1353            return self._model_model.item(row, 0).isCheckable()
1354
1355        # If multiple rows selected - toggle all of them, filtering uncheckable
1356        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1357
1358        # Switch off signaling from the model to avoid recursion
1359        self._model_model.blockSignals(True)
1360        # Convert to proper indices and set requested enablement
1361        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1362        self._model_model.blockSignals(False)
1363
1364        # update the list of parameters to fit
1365        main_params = self.checkedListFromModel(self._model_model)
1366        poly_params = self.checkedListFromModel(self._poly_model)
1367        magnet_params = self.checkedListFromModel(self._magnet_model)
1368
1369        # Retrieve poly params names
1370        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1371
1372        self.parameters_to_fit = main_params + poly_params + magnet_params
1373
1374    def checkedListFromModel(self, model):
1375        """
1376        Returns list of checked parameters for given model
1377        """
1378        def isChecked(row):
1379            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1380
1381        return [str(model.item(row_index, 0).text())
1382                for row_index in range(model.rowCount())
1383                if isChecked(row_index)]
1384
1385    def nameForFittedData(self, name):
1386        """
1387        Generate name for the current fit
1388        """
1389        if self.is2D:
1390            name += "2d"
1391        name = "M%i [%s]" % (self.tab_id, name)
1392        return name
1393
1394    def createNewIndex(self, fitted_data):
1395        """
1396        Create a model or theory index with passed Data1D/Data2D
1397        """
1398        if self.data_is_loaded:
1399            if not fitted_data.name:
1400                name = self.nameForFittedData(self.data.filename)
1401                fitted_data.title = name
1402                fitted_data.name = name
1403                fitted_data.filename = name
1404                fitted_data.symbol = "Line"
1405            self.updateModelIndex(fitted_data)
1406        else:
1407            name = self.nameForFittedData(self.kernel_module.name)
1408            fitted_data.title = name
1409            fitted_data.name = name
1410            fitted_data.filename = name
1411            fitted_data.symbol = "Line"
1412            self.createTheoryIndex(fitted_data)
1413
1414    def updateModelIndex(self, fitted_data):
1415        """
1416        Update a QStandardModelIndex containing model data
1417        """
1418        name = self.nameFromData(fitted_data)
1419        # Make this a line if no other defined
1420        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1421            fitted_data.symbol = 'Line'
1422        # Notify the GUI manager so it can update the main model in DataExplorer
1423        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
1424
1425    def createTheoryIndex(self, fitted_data):
1426        """
1427        Create a QStandardModelIndex containing model data
1428        """
1429        name = self.nameFromData(fitted_data)
1430        # Notify the GUI manager so it can create the theory model in DataExplorer
1431        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
1432        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1433
1434    def nameFromData(self, fitted_data):
1435        """
1436        Return name for the dataset. Terribly impure function.
1437        """
1438        if fitted_data.name is None:
1439            name = self.nameForFittedData(self.logic.data.filename)
1440            fitted_data.title = name
1441            fitted_data.name = name
1442            fitted_data.filename = name
1443        else:
1444            name = fitted_data.name
1445        return name
1446
1447    def methodCalculateForData(self):
1448        '''return the method for data calculation'''
1449        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1450
1451    def methodCompleteForData(self):
1452        '''return the method for result parsin on calc complete '''
1453        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1454
1455    def calculateQGridForModel(self):
1456        """
1457        Prepare the fitting data object, based on current ModelModel
1458        """
1459        if self.kernel_module is None:
1460            return
1461        # Awful API to a backend method.
1462        method = self.methodCalculateForData()(data=self.data,
1463                                               model=self.kernel_module,
1464                                               page_id=0,
1465                                               qmin=self.q_range_min,
1466                                               qmax=self.q_range_max,
1467                                               smearer=None,
1468                                               state=None,
1469                                               weight=None,
1470                                               fid=None,
1471                                               toggle_mode_on=False,
1472                                               completefn=None,
1473                                               update_chisqr=True,
1474                                               exception_handler=self.calcException,
1475                                               source=None)
1476
1477        calc_thread = threads.deferToThread(method.compute)
1478        calc_thread.addCallback(self.methodCompleteForData())
1479        calc_thread.addErrback(self.calculateDataFailed)
1480
1481    def calculateDataFailed(self, reason):
1482        """
1483        Thread returned error
1484        """
1485        print("Calculate Data failed with ", reason)
1486
1487    def complete1D(self, return_data):
1488        """
1489        Plot the current 1D data
1490        """
1491        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1492        self.calculateResiduals(fitted_data)
1493        self.model_data = fitted_data
1494
1495    def complete2D(self, return_data):
1496        """
1497        Plot the current 2D data
1498        """
1499        fitted_data = self.logic.new2DPlot(return_data)
1500        self.calculateResiduals(fitted_data)
1501        self.model_data = fitted_data
1502
1503    def calculateResiduals(self, fitted_data):
1504        """
1505        Calculate and print Chi2 and display chart of residuals
1506        """
1507        # Create a new index for holding data
1508        fitted_data.symbol = "Line"
1509
1510        # Modify fitted_data with weighting
1511        self.addWeightingToData(fitted_data)
1512
1513        self.createNewIndex(fitted_data)
1514        # Calculate difference between return_data and logic.data
1515        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1516        # Update the control
1517        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1518        self.lblChi2Value.setText(chi2_repr)
1519
1520        self.communicate.plotUpdateSignal.emit([fitted_data])
1521
1522        # Plot residuals if actual data
1523        if not self.data_is_loaded:
1524            return
1525
1526        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1527        residuals_plot.id = "Residual " + residuals_plot.id
1528        self.createNewIndex(residuals_plot)
1529        #self.communicate.plotUpdateSignal.emit([residuals_plot])
1530
1531    def calcException(self, etype, value, tb):
1532        """
1533        Thread threw an exception.
1534        """
1535        # TODO: remimplement thread cancellation
1536        logging.error("".join(traceback.format_exception(etype, value, tb)))
1537
1538    def setTableProperties(self, table):
1539        """
1540        Setting table properties
1541        """
1542        # Table properties
1543        table.verticalHeader().setVisible(False)
1544        table.setAlternatingRowColors(True)
1545        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1546        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
1547        table.resizeColumnsToContents()
1548
1549        # Header
1550        header = table.horizontalHeader()
1551        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
1552        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
1553
1554        # Qt5: the following 2 lines crash - figure out why!
1555        # Resize column 0 and 7 to content
1556        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
1557        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
1558
1559    def setPolyModel(self):
1560        """
1561        Set polydispersity values
1562        """
1563        if not self.model_parameters:
1564            return
1565        self._poly_model.clear()
1566
1567        [self.setPolyModelParameters(i, param) for i, param in \
1568            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1569        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1570
1571    def setPolyModelParameters(self, i, param):
1572        """
1573        Standard of multishell poly parameter driver
1574        """
1575        param_name = param.name
1576        # see it the parameter is multishell
1577        if '[' in param.name:
1578            # Skip empty shells
1579            if self.current_shell_displayed == 0:
1580                return
1581            else:
1582                # Create as many entries as current shells
1583                for ishell in range(1, self.current_shell_displayed+1):
1584                    # Remove [n] and add the shell numeral
1585                    name = param_name[0:param_name.index('[')] + str(ishell)
1586                    self.addNameToPolyModel(i, name)
1587        else:
1588            # Just create a simple param entry
1589            self.addNameToPolyModel(i, param_name)
1590
1591    def addNameToPolyModel(self, i, param_name):
1592        """
1593        Creates a checked row in the poly model with param_name
1594        """
1595        # Polydisp. values from the sasmodel
1596        width = self.kernel_module.getParam(param_name + '.width')
1597        npts = self.kernel_module.getParam(param_name + '.npts')
1598        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1599        _, min, max = self.kernel_module.details[param_name]
1600
1601        # Construct a row with polydisp. related variable.
1602        # This will get added to the polydisp. model
1603        # Note: last argument needs extra space padding for decent display of the control
1604        checked_list = ["Distribution of " + param_name, str(width),
1605                        str(min), str(max),
1606                        str(npts), str(nsigs), "gaussian      ",'']
1607        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1608
1609        # All possible polydisp. functions as strings in combobox
1610        func = QtWidgets.QComboBox()
1611        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
1612        # Set the default index
1613        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1614        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1615        self.lstPoly.setIndexWidget(ind, func)
1616        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1617
1618    def onPolyFilenameChange(self, row_index):
1619        """
1620        Respond to filename_updated signal from the delegate
1621        """
1622        # For the given row, invoke the "array" combo handler
1623        array_caption = 'array'
1624
1625        # Get the combo box reference
1626        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1627        widget = self.lstPoly.indexWidget(ind)
1628
1629        # Update the combo box so it displays "array"
1630        widget.blockSignals(True)
1631        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1632        widget.blockSignals(False)
1633
1634        # Invoke the file reader
1635        self.onPolyComboIndexChange(array_caption, row_index)
1636
1637    def onPolyComboIndexChange(self, combo_string, row_index):
1638        """
1639        Modify polydisp. defaults on function choice
1640        """
1641        # Get npts/nsigs for current selection
1642        param = self.model_parameters.form_volume_parameters[row_index]
1643        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1644        combo_box = self.lstPoly.indexWidget(file_index)
1645
1646        def updateFunctionCaption(row):
1647            # Utility function for update of polydispersity function name in the main model
1648            param_name = str(self._model_model.item(row, 0).text())
1649            if param_name !=  param.name:
1650                return
1651            # Modify the param value
1652            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1653
1654        if combo_string == 'array':
1655            try:
1656                self.loadPolydispArray(row_index)
1657                # Update main model for display
1658                self.iterateOverModel(updateFunctionCaption)
1659                # disable the row
1660                lo = self.lstPoly.itemDelegate().poly_pd
1661                hi = self.lstPoly.itemDelegate().poly_function
1662                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
1663                return
1664            except IOError:
1665                combo_box.setCurrentIndex(self.orig_poly_index)
1666                # Pass for cancel/bad read
1667                pass
1668
1669        # Enable the row in case it was disabled by Array
1670        self._poly_model.blockSignals(True)
1671        max_range = self.lstPoly.itemDelegate().poly_filename
1672        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
1673        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1674        self._poly_model.setData(file_index, "")
1675        self._poly_model.blockSignals(False)
1676
1677        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1678        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1679
1680        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1681        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1682
1683        self._poly_model.setData(npts_index, npts)
1684        self._poly_model.setData(nsigs_index, nsigs)
1685
1686        self.iterateOverModel(updateFunctionCaption)
1687        self.orig_poly_index = combo_box.currentIndex()
1688
1689    def loadPolydispArray(self, row_index):
1690        """
1691        Show the load file dialog and loads requested data into state
1692        """
1693        datafile = QtWidgets.QFileDialog.getOpenFileName(
1694            self, "Choose a weight file", "", "All files (*.*)", None,
1695            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
1696
1697        if not datafile:
1698            logging.info("No weight data chosen.")
1699            raise IOError
1700
1701        values = []
1702        weights = []
1703        def appendData(data_tuple):
1704            """
1705            Fish out floats from a tuple of strings
1706            """
1707            try:
1708                values.append(float(data_tuple[0]))
1709                weights.append(float(data_tuple[1]))
1710            except (ValueError, IndexError):
1711                # just pass through if line with bad data
1712                return
1713
1714        with open(datafile, 'r') as column_file:
1715            column_data = [line.rstrip().split() for line in column_file.readlines()]
1716            [appendData(line) for line in column_data]
1717
1718        # If everything went well - update the sasmodel values
1719        self.disp_model = POLYDISPERSITY_MODELS['array']()
1720        self.disp_model.set_weights(np.array(values), np.array(weights))
1721        # + update the cell with filename
1722        fname = os.path.basename(str(datafile))
1723        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1724        self._poly_model.setData(fname_index, fname)
1725
1726    def setMagneticModel(self):
1727        """
1728        Set magnetism values on model
1729        """
1730        if not self.model_parameters:
1731            return
1732        self._magnet_model.clear()
1733        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1734            self.model_parameters.call_parameters if param.type == 'magnetic']
1735        FittingUtilities.addHeadersToModel(self._magnet_model)
1736
1737    def shellNamesList(self):
1738        """
1739        Returns list of names of all multi-shell parameters
1740        E.g. for sld[n], radius[n], n=1..3 it will return
1741        [sld1, sld2, sld3, radius1, radius2, radius3]
1742        """
1743        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1744        top_index = self.kernel_module.multiplicity_info.number
1745        shell_names = []
1746        for i in range(1, top_index+1):
1747            for name in multi_names:
1748                shell_names.append(name+str(i))
1749        return shell_names
1750
1751    def addCheckedMagneticListToModel(self, param, model):
1752        """
1753        Wrapper for model update with a subset of magnetic parameters
1754        """
1755        if param.name[param.name.index(':')+1:] in self.shell_names:
1756            # check if two-digit shell number
1757            try:
1758                shell_index = int(param.name[-2:])
1759            except ValueError:
1760                shell_index = int(param.name[-1:])
1761
1762            if shell_index > self.current_shell_displayed:
1763                return
1764
1765        checked_list = [param.name,
1766                        str(param.default),
1767                        str(param.limits[0]),
1768                        str(param.limits[1]),
1769                        param.units]
1770
1771        FittingUtilities.addCheckedListToModel(model, checked_list)
1772
1773    def enableStructureFactorControl(self, structure_factor):
1774        """
1775        Add structure factors to the list of parameters
1776        """
1777        if self.kernel_module.is_form_factor or structure_factor == 'None':
1778            self.enableStructureCombo()
1779        else:
1780            self.disableStructureCombo()
1781
1782    def addExtraShells(self):
1783        """
1784        Add a combobox for multiple shell display
1785        """
1786        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1787
1788        if param_length == 0:
1789            return
1790
1791        # cell 1: variable name
1792        item1 = QtGui.QStandardItem(param_name)
1793
1794        func = QtWidgets.QComboBox()
1795        # Available range of shells displayed in the combobox
1796        func.addItems([str(i) for i in range(param_length+1)])
1797
1798        # Respond to index change
1799        func.currentIndexChanged.connect(self.modifyShellsInList)
1800
1801        # cell 2: combobox
1802        item2 = QtGui.QStandardItem()
1803        self._model_model.appendRow([item1, item2])
1804
1805        # Beautify the row:  span columns 2-4
1806        shell_row = self._model_model.rowCount()
1807        shell_index = self._model_model.index(shell_row-1, 1)
1808
1809        self.lstParams.setIndexWidget(shell_index, func)
1810        self._last_model_row = self._model_model.rowCount()
1811
1812        # Set the index to the state-kept value
1813        func.setCurrentIndex(self.current_shell_displayed
1814                             if self.current_shell_displayed < func.count() else 0)
1815
1816    def modifyShellsInList(self, index):
1817        """
1818        Add/remove additional multishell parameters
1819        """
1820        # Find row location of the combobox
1821        last_row = self._last_model_row
1822        remove_rows = self._model_model.rowCount() - last_row
1823
1824        if remove_rows > 1:
1825            self._model_model.removeRows(last_row, remove_rows)
1826
1827        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1828        self.current_shell_displayed = index
1829
1830        # Update relevant models
1831        self.setPolyModel()
1832        self.setMagneticModel()
1833
1834    def readFitPage(self, fp):
1835        """
1836        Read in state from a fitpage object and update GUI
1837        """
1838        assert isinstance(fp, FitPage)
1839        # Main tab info
1840        self.logic.data.filename = fp.filename
1841        self.data_is_loaded = fp.data_is_loaded
1842        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1843        self.chkMagnetism.setCheckState(fp.is_magnetic)
1844        self.chk2DView.setCheckState(fp.is2D)
1845
1846        # Update the comboboxes
1847        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1848        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1849        if fp.current_factor:
1850            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1851
1852        self.chi2 = fp.chi2
1853
1854        # Options tab
1855        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1856        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1857        self.npts = fp.fit_options[fp.NPTS]
1858        self.log_points = fp.fit_options[fp.LOG_POINTS]
1859        self.weighting = fp.fit_options[fp.WEIGHTING]
1860
1861        # Models
1862        self._model_model = fp.model_model
1863        self._poly_model = fp.poly_model
1864        self._magnet_model = fp.magnetism_model
1865
1866        # Resolution tab
1867        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1868        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1869        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1870        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1871        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1872
1873        # TODO: add polidyspersity and magnetism
1874
1875    def saveToFitPage(self, fp):
1876        """
1877        Write current state to the given fitpage
1878        """
1879        assert isinstance(fp, FitPage)
1880
1881        # Main tab info
1882        fp.filename = self.logic.data.filename
1883        fp.data_is_loaded = self.data_is_loaded
1884        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1885        fp.is_magnetic = self.chkMagnetism.isChecked()
1886        fp.is2D = self.chk2DView.isChecked()
1887        fp.data = self.data
1888
1889        # Use current models - they contain all the required parameters
1890        fp.model_model = self._model_model
1891        fp.poly_model = self._poly_model
1892        fp.magnetism_model = self._magnet_model
1893
1894        if self.cbCategory.currentIndex() != 0:
1895            fp.current_category = str(self.cbCategory.currentText())
1896            fp.current_model = str(self.cbModel.currentText())
1897
1898        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1899            fp.current_factor = str(self.cbStructureFactor.currentText())
1900        else:
1901            fp.current_factor = ''
1902
1903        fp.chi2 = self.chi2
1904        fp.parameters_to_fit = self.parameters_to_fit
1905        fp.kernel_module = self.kernel_module
1906
1907        # Algorithm options
1908        # fp.algorithm = self.parent.fit_options.selected_id
1909
1910        # Options tab
1911        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1912        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1913        fp.fit_options[fp.NPTS] = self.npts
1914        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1915        fp.fit_options[fp.LOG_POINTS] = self.log_points
1916        fp.fit_options[fp.WEIGHTING] = self.weighting
1917
1918        # Resolution tab
1919        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1920        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1921        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1922        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1923        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1924
1925        # TODO: add polidyspersity and magnetism
1926
1927
1928    def updateUndo(self):
1929        """
1930        Create a new state page and add it to the stack
1931        """
1932        if self.undo_supported:
1933            self.pushFitPage(self.currentState())
1934
1935    def currentState(self):
1936        """
1937        Return fit page with current state
1938        """
1939        new_page = FitPage()
1940        self.saveToFitPage(new_page)
1941
1942        return new_page
1943
1944    def pushFitPage(self, new_page):
1945        """
1946        Add a new fit page object with current state
1947        """
1948        self.page_stack.append(new_page)
1949
1950    def popFitPage(self):
1951        """
1952        Remove top fit page from stack
1953        """
1954        if self.page_stack:
1955            self.page_stack.pop()
1956
Note: See TracBrowser for help on using the repository browser.