source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 672c0e97

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

Magnetism is enabled only for selected few models. SASVIEW-1199

  • Property mode set to 100644
File size: 141.1 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5import copy
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10import webbrowser
11
12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
15
16from sasmodels import generate
17from sasmodels import modelinfo
18from sasmodels.sasview_model import load_standard_models
19from sasmodels.sasview_model import MultiplicationModel
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23from sas.sascalc.fit.pagestate import PageState
24
25import sas.qtgui.Utilities.GuiUtils as GuiUtils
26import sas.qtgui.Utilities.LocalConfig as LocalConfig
27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
30from sas.qtgui.Plotting.Plotter import PlotterWidget
31
32from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
33from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
34from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
35
36from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
38from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
39from sas.qtgui.Perspectives.Fitting import FittingUtilities
40from sas.qtgui.Perspectives.Fitting import ModelUtilities
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
47from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
48from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
49from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
50
51TAB_MAGNETISM = 4
52TAB_POLY = 3
53CATEGORY_DEFAULT = "Choose category..."
54CATEGORY_STRUCTURE = "Structure Factor"
55CATEGORY_CUSTOM = "Plugin Models"
56STRUCTURE_DEFAULT = "None"
57
58DEFAULT_POLYDISP_FUNCTION = 'gaussian'
59
60# CRUFT: remove when new release of sasmodels is available
61# https://github.com/SasView/sasview/pull/181#discussion_r218135162
62from sasmodels.sasview_model import SasviewModel
63if not hasattr(SasviewModel, 'get_weights'):
64    def get_weights(self, name):
65        """
66        Returns the polydispersity distribution for parameter *name* as *value* and *weight* arrays.
67        """
68        # type: (str) -> Tuple(np.ndarray, np.ndarray)
69        _, x, w = self._get_weights(self._model_info.parameters[name])
70        return x, w
71
72    SasviewModel.get_weights = get_weights
73
74logger = logging.getLogger(__name__)
75
76class ToolTippedItemModel(QtGui.QStandardItemModel):
77    """
78    Subclass from QStandardItemModel to allow displaying tooltips in
79    QTableView model.
80    """
81    def __init__(self, parent=None):
82        QtGui.QStandardItemModel.__init__(self, parent)
83
84    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
85        """
86        Displays tooltip for each column's header
87        :param section:
88        :param orientation:
89        :param role:
90        :return:
91        """
92        if role == QtCore.Qt.ToolTipRole:
93            if orientation == QtCore.Qt.Horizontal:
94                return str(self.header_tooltips[section])
95
96        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
97
98class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
99    """
100    Main widget for selecting form and structure factor models
101    """
102    constraintAddedSignal = QtCore.pyqtSignal(list)
103    newModelSignal = QtCore.pyqtSignal()
104    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
105    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
106    Calc1DFinishedSignal = QtCore.pyqtSignal(dict)
107    Calc2DFinishedSignal = QtCore.pyqtSignal(dict)
108
109    MAGNETIC_MODELS = ['sphere', 'core_shell_sphere', 'core_multi_shell', 'cylinder', 'parallelepiped']
110
111    def __init__(self, parent=None, data=None, tab_id=1):
112
113        super(FittingWidget, self).__init__()
114
115        # Necessary globals
116        self.parent = parent
117
118        # Which tab is this widget displayed in?
119        self.tab_id = tab_id
120
121        # Globals
122        self.initializeGlobals()
123
124        # data index for the batch set
125        self.data_index = 0
126        # Main Data[12]D holders
127        # Logics.data contains a single Data1D/Data2D object
128        self._logic = [FittingLogic()]
129
130        # Main GUI setup up
131        self.setupUi(self)
132        self.setWindowTitle("Fitting")
133
134        # Set up tabs widgets
135        self.initializeWidgets()
136
137        # Set up models and views
138        self.initializeModels()
139
140        # Defaults for the structure factors
141        self.setDefaultStructureCombo()
142
143        # Make structure factor and model CBs disabled
144        self.disableModelCombo()
145        self.disableStructureCombo()
146
147        # Generate the category list for display
148        self.initializeCategoryCombo()
149
150        # Initial control state
151        self.initializeControls()
152
153        QtWidgets.QApplication.processEvents()
154
155        # Connect signals to controls
156        self.initializeSignals()
157
158        if data is not None:
159            self.data = data
160
161        # New font to display angstrom symbol
162        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
163        self.label_17.setStyleSheet(new_font)
164        self.label_19.setStyleSheet(new_font)
165
166    @property
167    def logic(self):
168        # make sure the logic contains at least one element
169        assert self._logic
170        # logic connected to the currently shown data
171        return self._logic[self.data_index]
172
173    @property
174    def data(self):
175        return self.logic.data
176
177    @data.setter
178    def data(self, value):
179        """ data setter """
180        # Value is either a list of indices for batch fitting or a simple index
181        # for standard fitting. Assure we have a list, regardless.
182        if isinstance(value, list):
183            self.is_batch_fitting = True
184        else:
185            value = [value]
186
187        assert isinstance(value[0], QtGui.QStandardItem)
188
189        # Keep reference to all datasets for batch
190        self.all_data = value
191
192        # Create logics with data items
193        # Logics.data contains only a single Data1D/Data2D object
194        if len(value) == 1:
195            # single data logic is already defined, update data on it
196            self._logic[0].data = GuiUtils.dataFromItem(value[0])
197        else:
198            # batch datasets
199            self._logic = []
200            for data_item in value:
201                logic = FittingLogic(data=GuiUtils.dataFromItem(data_item))
202                self._logic.append(logic)
203
204        # Overwrite data type descriptor
205
206        self.is2D = True if isinstance(self.logic.data, Data2D) else False
207
208        # Let others know we're full of data now
209        self.data_is_loaded = True
210
211        # Enable/disable UI components
212        self.setEnablementOnDataLoad()
213
214    def initializeGlobals(self):
215        """
216        Initialize global variables used in this class
217        """
218        # SasModel is loaded
219        self.model_is_loaded = False
220        # Data[12]D passed and set
221        self.data_is_loaded = False
222        # Batch/single fitting
223        self.is_batch_fitting = False
224        self.is_chain_fitting = False
225        # Is the fit job running?
226        self.fit_started = False
227        # The current fit thread
228        self.calc_fit = None
229        # Current SasModel in view
230        self.kernel_module = None
231        # Current SasModel view dimension
232        self.is2D = False
233        # Current SasModel is multishell
234        self.model_has_shells = False
235        # Utility variable to enable unselectable option in category combobox
236        self._previous_category_index = 0
237        # Utility variables for multishell display
238        self._n_shells_row = 0
239        self._num_shell_params = 0
240        # Dictionary of {model name: model class} for the current category
241        self.models = {}
242        # Parameters to fit
243        self.main_params_to_fit = []
244        self.poly_params_to_fit = []
245        self.magnet_params_to_fit = []
246
247        # Fit options
248        self.q_range_min = 0.005
249        self.q_range_max = 0.1
250        self.npts = 25
251        self.log_points = False
252        self.weighting = 0
253        self.chi2 = None
254        # Does the control support UNDO/REDO
255        # temporarily off
256        self.undo_supported = False
257        self.page_stack = []
258        self.all_data = []
259        # custom plugin models
260        # {model.name:model}
261        self.custom_models = self.customModels()
262        # Polydisp widget table default index for function combobox
263        self.orig_poly_index = 3
264        # copy of current kernel model
265        self.kernel_module_copy = None
266
267        # dictionaries of current params
268        self.poly_params = {}
269        self.magnet_params = {}
270
271        # Page id for fitting
272        # To keep with previous SasView values, use 200 as the start offset
273        self.page_id = 200 + self.tab_id
274
275        # Data for chosen model
276        self.model_data = None
277
278        # Which shell is being currently displayed?
279        self.current_shell_displayed = 0
280        # List of all shell-unique parameters
281        self.shell_names = []
282
283        # Error column presence in parameter display
284        self.has_error_column = False
285        self.has_poly_error_column = False
286        self.has_magnet_error_column = False
287
288        # If the widget generated theory item, save it
289        self.theory_item = None
290
291        # list column widths
292        self.lstParamHeaderSizes = {}
293
294        # signal communicator
295        self.communicate = self.parent.communicate
296
297    def initializeWidgets(self):
298        """
299        Initialize widgets for tabs
300        """
301        # Options widget
302        layout = QtWidgets.QGridLayout()
303        self.options_widget = OptionsWidget(self, self.logic)
304        layout.addWidget(self.options_widget)
305        self.tabOptions.setLayout(layout)
306
307        # Smearing widget
308        layout = QtWidgets.QGridLayout()
309        self.smearing_widget = SmearingWidget(self)
310        layout.addWidget(self.smearing_widget)
311        self.tabResolution.setLayout(layout)
312
313        # Define bold font for use in various controls
314        self.boldFont = QtGui.QFont()
315        self.boldFont.setBold(True)
316
317        # Set data label
318        self.label.setFont(self.boldFont)
319        self.label.setText("No data loaded")
320        self.lblFilename.setText("")
321
322        # Magnetic angles explained in one picture
323        self.magneticAnglesWidget = QtWidgets.QWidget()
324        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
325        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.png')
326        labl.setPixmap(pixmap)
327        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
328
329    def initializeModels(self):
330        """
331        Set up models and views
332        """
333        # Set the main models
334        # We can't use a single model here, due to restrictions on flattening
335        # the model tree with subclassed QAbstractProxyModel...
336        self._model_model = ToolTippedItemModel()
337        self._poly_model = ToolTippedItemModel()
338        self._magnet_model = ToolTippedItemModel()
339
340        # Param model displayed in param list
341        self.lstParams.setModel(self._model_model)
342        self.readCategoryInfo()
343
344        self.model_parameters = None
345
346        # Delegates for custom editing and display
347        self.lstParams.setItemDelegate(ModelViewDelegate(self))
348
349        self.lstParams.setAlternatingRowColors(True)
350        stylesheet = """
351
352            QTreeView {
353                paint-alternating-row-colors-for-empty-area:0;
354            }
355
356            QTreeView::item {
357                border: 1px;
358                padding: 2px 1px;
359            }
360
361            QTreeView::item:hover {
362                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
363                border: 1px solid #bfcde4;
364            }
365
366            QTreeView::item:selected {
367                border: 1px solid #567dbc;
368            }
369
370            QTreeView::item:selected:active{
371                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
372            }
373
374            QTreeView::item:selected:!active {
375                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
376            }
377           """
378        self.lstParams.setStyleSheet(stylesheet)
379        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
380        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
381        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
382        # Column resize signals
383        self.lstParams.header().sectionResized.connect(self.onColumnWidthUpdate)
384
385        # Poly model displayed in poly list
386        self.lstPoly.setModel(self._poly_model)
387        self.setPolyModel()
388        self.setTableProperties(self.lstPoly)
389        # Delegates for custom editing and display
390        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
391        # Polydispersity function combo response
392        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
393        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
394
395        # Magnetism model displayed in magnetism list
396        self.lstMagnetic.setModel(self._magnet_model)
397        self.setMagneticModel()
398        self.setTableProperties(self.lstMagnetic)
399        # Delegates for custom editing and display
400        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
401
402    def initializeCategoryCombo(self):
403        """
404        Model category combo setup
405        """
406        category_list = sorted(self.master_category_dict.keys())
407        self.cbCategory.addItem(CATEGORY_DEFAULT)
408        self.cbCategory.addItems(category_list)
409        if CATEGORY_STRUCTURE not in category_list:
410            self.cbCategory.addItem(CATEGORY_STRUCTURE)
411        self.cbCategory.setCurrentIndex(0)
412
413    def setEnablementOnDataLoad(self):
414        """
415        Enable/disable various UI elements based on data loaded
416        """
417        # Tag along functionality
418        self.label.setText("Data loaded from: ")
419        if self.logic.data.filename:
420            self.lblFilename.setText(self.logic.data.filename)
421        else:
422            self.lblFilename.setText(self.logic.data.name)
423        self.updateQRange()
424        # Switch off Data2D control
425        self.chk2DView.setEnabled(False)
426        self.chk2DView.setVisible(False)
427        self.chkMagnetism.setEnabled(False)
428        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked())
429        # Combo box or label for file name"
430        if self.is_batch_fitting:
431            self.lblFilename.setVisible(False)
432            for dataitem in self.all_data:
433                filename = GuiUtils.dataFromItem(dataitem).filename
434                self.cbFileNames.addItem(filename)
435            self.cbFileNames.setVisible(True)
436            self.chkChainFit.setEnabled(True)
437            self.chkChainFit.setVisible(True)
438            # This panel is not designed to view individual fits, so disable plotting
439            self.cmdPlot.setVisible(False)
440        # Similarly on other tabs
441        self.options_widget.setEnablementOnDataLoad()
442        self.onSelectModel()
443        # Smearing tab
444        self.smearing_widget.updateData(self.data)
445
446    def acceptsData(self):
447        """ Tells the caller this widget can accept new dataset """
448        return not self.data_is_loaded
449
450    def disableModelCombo(self):
451        """ Disable the combobox """
452        self.cbModel.setEnabled(False)
453        self.lblModel.setEnabled(False)
454
455    def enableModelCombo(self):
456        """ Enable the combobox """
457        self.cbModel.setEnabled(True)
458        self.lblModel.setEnabled(True)
459
460    def disableStructureCombo(self):
461        """ Disable the combobox """
462        self.cbStructureFactor.setEnabled(False)
463        self.lblStructure.setEnabled(False)
464
465    def enableStructureCombo(self):
466        """ Enable the combobox """
467        self.cbStructureFactor.setEnabled(True)
468        self.lblStructure.setEnabled(True)
469
470    def togglePoly(self, isChecked):
471        """ Enable/disable the polydispersity tab """
472        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
473        # Check if any parameters are ready for fitting
474        self.cmdFit.setEnabled(self.haveParamsToFit())
475
476    def toggleMagnetism(self, isChecked):
477        """ Enable/disable the magnetism tab """
478        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
479        # Check if any parameters are ready for fitting
480        self.cmdFit.setEnabled(self.haveParamsToFit())
481
482    def toggleChainFit(self, isChecked):
483        """ Enable/disable chain fitting """
484        self.is_chain_fitting = isChecked
485
486    def toggle2D(self, isChecked):
487        """ Enable/disable the controls dependent on 1D/2D data instance """
488        self.chkMagnetism.setEnabled(isChecked)
489        self.is2D = isChecked
490        # Reload the current model
491        if self.kernel_module:
492            self.onSelectModel()
493
494    @classmethod
495    def customModels(cls):
496        """ Reads in file names in the custom plugin directory """
497        return ModelUtilities._find_models()
498
499    def initializeControls(self):
500        """
501        Set initial control enablement
502        """
503        self.cbFileNames.setVisible(False)
504        self.cmdFit.setEnabled(False)
505        self.cmdPlot.setEnabled(False)
506        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
507        self.chkPolydispersity.setEnabled(True)
508        self.chkPolydispersity.setCheckState(False)
509        self.chk2DView.setEnabled(True)
510        self.chk2DView.setCheckState(False)
511        self.chkMagnetism.setEnabled(False)
512        self.chkMagnetism.setCheckState(False)
513        self.chkChainFit.setEnabled(False)
514        self.chkChainFit.setVisible(False)
515        # Tabs
516        self.tabFitting.setTabEnabled(TAB_POLY, False)
517        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
518        self.lblChi2Value.setText("---")
519        # Smearing tab
520        self.smearing_widget.updateData(self.data)
521        # Line edits in the option tab
522        self.updateQRange()
523
524    def initializeSignals(self):
525        """
526        Connect GUI element signals
527        """
528        # Comboboxes
529        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
530        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
531        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
532        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
533        # Checkboxes
534        self.chk2DView.toggled.connect(self.toggle2D)
535        self.chkPolydispersity.toggled.connect(self.togglePoly)
536        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
537        self.chkChainFit.toggled.connect(self.toggleChainFit)
538        # Buttons
539        self.cmdFit.clicked.connect(self.onFit)
540        self.cmdPlot.clicked.connect(self.onPlot)
541        self.cmdHelp.clicked.connect(self.onHelp)
542        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
543
544        # Respond to change in parameters from the UI
545        self._model_model.itemChanged.connect(self.onMainParamsChange)
546        #self.constraintAddedSignal.connect(self.modifyViewOnConstraint)
547        self._poly_model.itemChanged.connect(self.onPolyModelChange)
548        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
549        self.lstParams.selectionModel().selectionChanged.connect(self.onSelectionChanged)
550
551        # Local signals
552        self.batchFittingFinishedSignal.connect(self.batchFitComplete)
553        self.fittingFinishedSignal.connect(self.fitComplete)
554        self.Calc1DFinishedSignal.connect(self.complete1D)
555        self.Calc2DFinishedSignal.connect(self.complete2D)
556
557        # Signals from separate tabs asking for replot
558        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
559
560        # Signals from other widgets
561        self.communicate.customModelDirectoryChanged.connect(self.onCustomModelChange)
562        self.communicate.saveAnalysisSignal.connect(self.savePageState)
563        self.smearing_widget.smearingChangedSignal.connect(self.onSmearingOptionsUpdate)
564
565        # Communicator signal
566        self.communicate.updateModelCategoriesSignal.connect(self.onCategoriesChanged)
567
568    def modelName(self):
569        """
570        Returns model name, by default M<tab#>, e.g. M1, M2
571        """
572        return "M%i" % self.tab_id
573
574    def nameForFittedData(self, name):
575        """
576        Generate name for the current fit
577        """
578        if self.is2D:
579            name += "2d"
580        name = "%s [%s]" % (self.modelName(), name)
581        return name
582
583    def showModelContextMenu(self, position):
584        """
585        Show context specific menu in the parameter table.
586        When clicked on parameter(s): fitting/constraints options
587        When clicked on white space: model description
588        """
589        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows()
590                if self.isCheckable(s.row())]
591        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
592        try:
593            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
594        except AttributeError as ex:
595            logger.error("Error generating context menu: %s" % ex)
596        return
597
598    def modelContextMenu(self, rows):
599        """
600        Create context menu for the parameter selection
601        """
602        menu = QtWidgets.QMenu()
603        num_rows = len(rows)
604        if num_rows < 1:
605            return menu
606        # Select for fitting
607        param_string = "parameter " if num_rows == 1 else "parameters "
608        to_string = "to its current value" if num_rows == 1 else "to their current values"
609        has_constraints = any([self.rowHasConstraint(i) for i in rows])
610
611        self.actionSelect = QtWidgets.QAction(self)
612        self.actionSelect.setObjectName("actionSelect")
613        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
614        # Unselect from fitting
615        self.actionDeselect = QtWidgets.QAction(self)
616        self.actionDeselect.setObjectName("actionDeselect")
617        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
618
619        self.actionConstrain = QtWidgets.QAction(self)
620        self.actionConstrain.setObjectName("actionConstrain")
621        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain "+param_string + to_string))
622
623        self.actionRemoveConstraint = QtWidgets.QAction(self)
624        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
625        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove constraint"))
626
627        self.actionMultiConstrain = QtWidgets.QAction(self)
628        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
629        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
630
631        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
632        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
633        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
634
635        menu.addAction(self.actionSelect)
636        menu.addAction(self.actionDeselect)
637        menu.addSeparator()
638
639        if has_constraints:
640            menu.addAction(self.actionRemoveConstraint)
641            #if num_rows == 1:
642            #    menu.addAction(self.actionEditConstraint)
643        else:
644            menu.addAction(self.actionConstrain)
645            if num_rows == 2:
646                menu.addAction(self.actionMutualMultiConstrain)
647
648        # Define the callbacks
649        self.actionConstrain.triggered.connect(self.addSimpleConstraint)
650        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
651        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
652        self.actionSelect.triggered.connect(self.selectParameters)
653        self.actionDeselect.triggered.connect(self.deselectParameters)
654        return menu
655
656    def showMultiConstraint(self):
657        """
658        Show the constraint widget and receive the expression
659        """
660        selected_rows = self.lstParams.selectionModel().selectedRows()
661        # There have to be only two rows selected. The caller takes care of that
662        # but let's check the correctness.
663        assert len(selected_rows) == 2
664
665        params_list = [s.data() for s in selected_rows]
666        # Create and display the widget for param1 and param2
667        mc_widget = MultiConstraint(self, params=params_list)
668        # Check if any of the parameters are polydisperse
669        if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
670            # no parameters are pd - reset the text to not show the warning
671            mc_widget.lblWarning.setText("")
672        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
673            return
674
675        constraint = Constraint()
676        c_text = mc_widget.txtConstraint.text()
677
678        # widget.params[0] is the parameter we're constraining
679        constraint.param = mc_widget.params[0]
680        # parameter should have the model name preamble
681        model_name = self.kernel_module.name
682        # param_used is the parameter we're using in constraining function
683        param_used = mc_widget.params[1]
684        # Replace param_used with model_name.param_used
685        updated_param_used = model_name + "." + param_used
686        new_func = c_text.replace(param_used, updated_param_used)
687        constraint.func = new_func
688        # Which row is the constrained parameter in?
689        row = self.getRowFromName(constraint.param)
690
691        # Create a new item and add the Constraint object as a child
692        self.addConstraintToRow(constraint=constraint, row=row)
693
694    def getRowFromName(self, name):
695        """
696        Given parameter name get the row number in self._model_model
697        """
698        for row in range(self._model_model.rowCount()):
699            row_name = self._model_model.item(row).text()
700            if row_name == name:
701                return row
702        return None
703
704    def getParamNames(self):
705        """
706        Return list of all parameters for the current model
707        """
708        return [self._model_model.item(row).text()
709                for row in range(self._model_model.rowCount())
710                if self.isCheckable(row)]
711
712    def modifyViewOnRow(self, row, font=None, brush=None):
713        """
714        Chage how the given row of the main model is shown
715        """
716        fields_enabled = False
717        if font is None:
718            font = QtGui.QFont()
719            fields_enabled = True
720        if brush is None:
721            brush = QtGui.QBrush()
722            fields_enabled = True
723        self._model_model.blockSignals(True)
724        # Modify font and foreground of affected rows
725        for column in range(0, self._model_model.columnCount()):
726            self._model_model.item(row, column).setForeground(brush)
727            self._model_model.item(row, column).setFont(font)
728            self._model_model.item(row, column).setEditable(fields_enabled)
729        self._model_model.blockSignals(False)
730
731    def addConstraintToRow(self, constraint=None, row=0):
732        """
733        Adds the constraint object to requested row
734        """
735        # Create a new item and add the Constraint object as a child
736        assert isinstance(constraint, Constraint)
737        assert 0 <= row <= self._model_model.rowCount()
738        assert self.isCheckable(row)
739
740        item = QtGui.QStandardItem()
741        item.setData(constraint)
742        self._model_model.item(row, 1).setChild(0, item)
743        # Set min/max to the value constrained
744        self.constraintAddedSignal.emit([row])
745        # Show visual hints for the constraint
746        font = QtGui.QFont()
747        font.setItalic(True)
748        brush = QtGui.QBrush(QtGui.QColor('blue'))
749        self.modifyViewOnRow(row, font=font, brush=brush)
750        self.communicate.statusBarUpdateSignal.emit('Constraint added')
751
752    def addSimpleConstraint(self):
753        """
754        Adds a constraint on a single parameter.
755        """
756        min_col = self.lstParams.itemDelegate().param_min
757        max_col = self.lstParams.itemDelegate().param_max
758        for row in self.selectedParameters():
759            assert(self.isCheckable(row))
760            param = self._model_model.item(row, 0).text()
761            value = self._model_model.item(row, 1).text()
762            min_t = self._model_model.item(row, min_col).text()
763            max_t = self._model_model.item(row, max_col).text()
764            # Create a Constraint object
765            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
766            # Create a new item and add the Constraint object as a child
767            item = QtGui.QStandardItem()
768            item.setData(constraint)
769            self._model_model.item(row, 1).setChild(0, item)
770            # Assumed correctness from the validator
771            value = float(value)
772            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
773            min_v = value - (value/10000.0)
774            max_v = value + (value/10000.0)
775            # Set min/max to the value constrained
776            self._model_model.item(row, min_col).setText(str(min_v))
777            self._model_model.item(row, max_col).setText(str(max_v))
778            self.constraintAddedSignal.emit([row])
779            # Show visual hints for the constraint
780            font = QtGui.QFont()
781            font.setItalic(True)
782            brush = QtGui.QBrush(QtGui.QColor('blue'))
783            self.modifyViewOnRow(row, font=font, brush=brush)
784        self.communicate.statusBarUpdateSignal.emit('Constraint added')
785
786    def deleteConstraint(self):
787        """
788        Delete constraints from selected parameters.
789        """
790        params = [s.data() for s in self.lstParams.selectionModel().selectedRows()
791                   if self.isCheckable(s.row())]
792        for param in params:
793            self.deleteConstraintOnParameter(param=param)
794
795    def deleteConstraintOnParameter(self, param=None):
796        """
797        Delete the constraint on model parameter 'param'
798        """
799        min_col = self.lstParams.itemDelegate().param_min
800        max_col = self.lstParams.itemDelegate().param_max
801        for row in range(self._model_model.rowCount()):
802            if not self.isCheckable(row):
803                continue
804            if not self.rowHasConstraint(row):
805                continue
806            # Get the Constraint object from of the model item
807            item = self._model_model.item(row, 1)
808            constraint = self.getConstraintForRow(row)
809            if constraint is None:
810                continue
811            if not isinstance(constraint, Constraint):
812                continue
813            if param and constraint.param != param:
814                continue
815            # Now we got the right row. Delete the constraint and clean up
816            # Retrieve old values and put them on the model
817            if constraint.min is not None:
818                self._model_model.item(row, min_col).setText(constraint.min)
819            if constraint.max is not None:
820                self._model_model.item(row, max_col).setText(constraint.max)
821            # Remove constraint item
822            item.removeRow(0)
823            self.constraintAddedSignal.emit([row])
824            self.modifyViewOnRow(row)
825
826        self.communicate.statusBarUpdateSignal.emit('Constraint removed')
827
828    def getConstraintForRow(self, row):
829        """
830        For the given row, return its constraint, if any (otherwise None)
831        """
832        if not self.isCheckable(row):
833            return None
834        item = self._model_model.item(row, 1)
835        try:
836            return item.child(0).data()
837        except AttributeError:
838            return None
839
840    def rowHasConstraint(self, row):
841        """
842        Finds out if row of the main model has a constraint child
843        """
844        if not self.isCheckable(row):
845            return False
846        item = self._model_model.item(row, 1)
847        if not item.hasChildren():
848            return False
849        c = item.child(0).data()
850        if isinstance(c, Constraint):
851            return True
852        return False
853
854    def rowHasActiveConstraint(self, row):
855        """
856        Finds out if row of the main model has an active constraint child
857        """
858        if not self.isCheckable(row):
859            return False
860        item = self._model_model.item(row, 1)
861        if not item.hasChildren():
862            return False
863        c = item.child(0).data()
864        if isinstance(c, Constraint) and c.active:
865            return True
866        return False
867
868    def rowHasActiveComplexConstraint(self, row):
869        """
870        Finds out if row of the main model has an active, nontrivial constraint child
871        """
872        if not self.isCheckable(row):
873            return False
874        item = self._model_model.item(row, 1)
875        if not item.hasChildren():
876            return False
877        c = item.child(0).data()
878        if isinstance(c, Constraint) and c.func and c.active:
879            return True
880        return False
881
882    def selectParameters(self):
883        """
884        Selected parameter is chosen for fitting
885        """
886        status = QtCore.Qt.Checked
887        self.setParameterSelection(status)
888
889    def deselectParameters(self):
890        """
891        Selected parameters are removed for fitting
892        """
893        status = QtCore.Qt.Unchecked
894        self.setParameterSelection(status)
895
896    def selectedParameters(self):
897        """ Returns list of selected (highlighted) parameters """
898        return [s.row() for s in self.lstParams.selectionModel().selectedRows()
899                if self.isCheckable(s.row())]
900
901    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
902        """
903        Selected parameters are chosen for fitting
904        """
905        # Convert to proper indices and set requested enablement
906        for row in self.selectedParameters():
907            self._model_model.item(row, 0).setCheckState(status)
908
909    def getConstraintsForModel(self):
910        """
911        Return a list of tuples. Each tuple contains constraints mapped as
912        ('constrained parameter', 'function to constrain')
913        e.g. [('sld','5*sld_solvent')]
914        """
915        param_number = self._model_model.rowCount()
916        params = [(self._model_model.item(s, 0).text(),
917                    self._model_model.item(s, 1).child(0).data().func)
918                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
919        return params
920
921    def getComplexConstraintsForModel(self):
922        """
923        Return a list of tuples. Each tuple contains constraints mapped as
924        ('constrained parameter', 'function to constrain')
925        e.g. [('sld','5*M2.sld_solvent')].
926        Only for constraints with defined VALUE
927        """
928        param_number = self._model_model.rowCount()
929        params = [(self._model_model.item(s, 0).text(),
930                    self._model_model.item(s, 1).child(0).data().func)
931                    for s in range(param_number) if self.rowHasActiveComplexConstraint(s)]
932        return params
933
934    def getConstraintObjectsForModel(self):
935        """
936        Returns Constraint objects present on the whole model
937        """
938        param_number = self._model_model.rowCount()
939        constraints = [self._model_model.item(s, 1).child(0).data()
940                       for s in range(param_number) if self.rowHasConstraint(s)]
941
942        return constraints
943
944    def getConstraintsForFitting(self):
945        """
946        Return a list of constraints in format ready for use in fiting
947        """
948        # Get constraints
949        constraints = self.getComplexConstraintsForModel()
950        # See if there are any constraints across models
951        multi_constraints = [cons for cons in constraints if self.isConstraintMultimodel(cons[1])]
952
953        if multi_constraints:
954            # Let users choose what to do
955            msg = "The current fit contains constraints relying on other fit pages.\n"
956            msg += "Parameters with those constraints are:\n" +\
957                '\n'.join([cons[0] for cons in multi_constraints])
958            msg += "\n\nWould you like to remove these constraints or cancel fitting?"
959            msgbox = QtWidgets.QMessageBox(self)
960            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
961            msgbox.setText(msg)
962            msgbox.setWindowTitle("Existing Constraints")
963            # custom buttons
964            button_remove = QtWidgets.QPushButton("Remove")
965            msgbox.addButton(button_remove, QtWidgets.QMessageBox.YesRole)
966            button_cancel = QtWidgets.QPushButton("Cancel")
967            msgbox.addButton(button_cancel, QtWidgets.QMessageBox.RejectRole)
968            retval = msgbox.exec_()
969            if retval == QtWidgets.QMessageBox.RejectRole:
970                # cancel fit
971                raise ValueError("Fitting cancelled")
972            else:
973                # remove constraint
974                for cons in multi_constraints:
975                    self.deleteConstraintOnParameter(param=cons[0])
976                # re-read the constraints
977                constraints = self.getComplexConstraintsForModel()
978
979        return constraints
980
981    def showModelDescription(self):
982        """
983        Creates a window with model description, when right clicked in the treeview
984        """
985        msg = 'Model description:\n'
986        if self.kernel_module is not None:
987            if str(self.kernel_module.description).rstrip().lstrip() == '':
988                msg += "Sorry, no information is available for this model."
989            else:
990                msg += self.kernel_module.description + '\n'
991        else:
992            msg += "You must select a model to get information on this"
993
994        menu = QtWidgets.QMenu()
995        label = QtWidgets.QLabel(msg)
996        action = QtWidgets.QWidgetAction(self)
997        action.setDefaultWidget(label)
998        menu.addAction(action)
999        return menu
1000
1001    def onSelectModel(self):
1002        """
1003        Respond to select Model from list event
1004        """
1005        model = self.cbModel.currentText()
1006
1007        # Assure the control is active
1008        if not self.cbModel.isEnabled():
1009            return
1010        # Empty combobox forced to be read
1011        if not model:
1012            return
1013        self.chkMagnetism.setEnabled(self.is2D and model in self.MAGNETIC_MODELS)
1014        # Reset parameters to fit
1015        self.resetParametersToFit()
1016        self.has_error_column = False
1017        self.has_poly_error_column = False
1018
1019        structure = None
1020        if self.cbStructureFactor.isEnabled():
1021            structure = str(self.cbStructureFactor.currentText())
1022        self.respondToModelStructure(model=model, structure_factor=structure)
1023
1024    def onSelectBatchFilename(self, data_index):
1025        """
1026        Update the logic based on the selected file in batch fitting
1027        """
1028        self.data_index = data_index
1029        self.updateQRange()
1030
1031    def onSelectStructureFactor(self):
1032        """
1033        Select Structure Factor from list
1034        """
1035        model = str(self.cbModel.currentText())
1036        category = str(self.cbCategory.currentText())
1037        structure = str(self.cbStructureFactor.currentText())
1038        if category == CATEGORY_STRUCTURE:
1039            model = None
1040
1041        # Reset parameters to fit
1042        self.resetParametersToFit()
1043        self.has_error_column = False
1044        self.has_poly_error_column = False
1045
1046        self.respondToModelStructure(model=model, structure_factor=structure)
1047
1048    def resetParametersToFit(self):
1049        """
1050        Clears the list of parameters to be fitted
1051        """
1052        self.main_params_to_fit = []
1053        self.poly_params_to_fit = []
1054        self.magnet_params_to_fit = []
1055
1056    def onCustomModelChange(self):
1057        """
1058        Reload the custom model combobox
1059        """
1060        self.custom_models = self.customModels()
1061        self.readCustomCategoryInfo()
1062        self.onCategoriesChanged()
1063
1064        # See if we need to update the combo in-place
1065        if self.cbCategory.currentText() != CATEGORY_CUSTOM: return
1066
1067        current_text = self.cbModel.currentText()
1068        self.cbModel.blockSignals(True)
1069        self.cbModel.clear()
1070        self.cbModel.blockSignals(False)
1071        self.enableModelCombo()
1072        self.disableStructureCombo()
1073        # Retrieve the list of models
1074        model_list = self.master_category_dict[CATEGORY_CUSTOM]
1075        # Populate the models combobox
1076        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1077        new_index = self.cbModel.findText(current_text)
1078        if new_index != -1:
1079            self.cbModel.setCurrentIndex(self.cbModel.findText(current_text))
1080
1081    def onSelectionChanged(self):
1082        """
1083        React to parameter selection
1084        """
1085        rows = self.lstParams.selectionModel().selectedRows()
1086        # Clean previous messages
1087        self.communicate.statusBarUpdateSignal.emit("")
1088        if len(rows) == 1:
1089            # Show constraint, if present
1090            row = rows[0].row()
1091            if not self.rowHasConstraint(row):
1092                return
1093            constr = self.getConstraintForRow(row)
1094            func = self.getConstraintForRow(row).func
1095            if constr.func is not None:
1096                # inter-parameter constraint
1097                update_text = "Active constraint: "+func
1098            elif constr.param == rows[0].data():
1099                # current value constraint
1100                update_text = "Value constrained to: " + str(constr.value)
1101            else:
1102                # ill defined constraint
1103                return
1104            self.communicate.statusBarUpdateSignal.emit(update_text)
1105
1106    def replaceConstraintName(self, old_name, new_name=""):
1107        """
1108        Replace names of models in defined constraints
1109        """
1110        param_number = self._model_model.rowCount()
1111        # loop over parameters
1112        for row in range(param_number):
1113            if self.rowHasConstraint(row):
1114                func = self._model_model.item(row, 1).child(0).data().func
1115                if old_name in func:
1116                    new_func = func.replace(old_name, new_name)
1117                    self._model_model.item(row, 1).child(0).data().func = new_func
1118
1119    def isConstraintMultimodel(self, constraint):
1120        """
1121        Check if the constraint function text contains current model name
1122        """
1123        current_model_name = self.kernel_module.name
1124        if current_model_name in constraint:
1125            return False
1126        else:
1127            return True
1128
1129    def updateData(self):
1130        """
1131        Helper function for recalculation of data used in plotting
1132        """
1133        # Update the chart
1134        if self.data_is_loaded:
1135            self.cmdPlot.setText("Show Plot")
1136            self.calculateQGridForModel()
1137        else:
1138            self.cmdPlot.setText("Calculate")
1139            # Create default datasets if no data passed
1140            self.createDefaultDataset()
1141            self.theory_item = None # ensure theory is recalc. before plot, see showTheoryPlot()
1142
1143    def respondToModelStructure(self, model=None, structure_factor=None):
1144        # Set enablement on calculate/plot
1145        self.cmdPlot.setEnabled(True)
1146
1147        # kernel parameters -> model_model
1148        self.SASModelToQModel(model, structure_factor)
1149
1150        for column, width in self.lstParamHeaderSizes.items():
1151            self.lstParams.setColumnWidth(column, width)
1152
1153        # Update plot
1154        self.updateData()
1155
1156        # Update state stack
1157        self.updateUndo()
1158
1159        # Let others know
1160        self.newModelSignal.emit()
1161
1162    def onSelectCategory(self):
1163        """
1164        Select Category from list
1165        """
1166        category = self.cbCategory.currentText()
1167        # Check if the user chose "Choose category entry"
1168        if category == CATEGORY_DEFAULT:
1169            # if the previous category was not the default, keep it.
1170            # Otherwise, just return
1171            if self._previous_category_index != 0:
1172                # We need to block signals, or else state changes on perceived unchanged conditions
1173                self.cbCategory.blockSignals(True)
1174                self.cbCategory.setCurrentIndex(self._previous_category_index)
1175                self.cbCategory.blockSignals(False)
1176            return
1177
1178        if category == CATEGORY_STRUCTURE:
1179            self.disableModelCombo()
1180            self.enableStructureCombo()
1181            # set the index to 0
1182            self.cbStructureFactor.setCurrentIndex(0)
1183            self.model_parameters = None
1184            self._model_model.clear()
1185            return
1186
1187        # Safely clear and enable the model combo
1188        self.cbModel.blockSignals(True)
1189        self.cbModel.clear()
1190        self.cbModel.blockSignals(False)
1191        self.enableModelCombo()
1192        self.disableStructureCombo()
1193
1194        self._previous_category_index = self.cbCategory.currentIndex()
1195        # Retrieve the list of models
1196        model_list = self.master_category_dict[category]
1197        # Populate the models combobox
1198        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1199
1200    def onPolyModelChange(self, item):
1201        """
1202        Callback method for updating the main model and sasmodel
1203        parameters with the GUI values in the polydispersity view
1204        """
1205        model_column = item.column()
1206        model_row = item.row()
1207        name_index = self._poly_model.index(model_row, 0)
1208        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1209        if "istribution of" in parameter_name:
1210            # just the last word
1211            parameter_name = parameter_name.rsplit()[-1]
1212
1213        delegate = self.lstPoly.itemDelegate()
1214
1215        # Extract changed value.
1216        if model_column == delegate.poly_parameter:
1217            # Is the parameter checked for fitting?
1218            value = item.checkState()
1219            parameter_name = parameter_name + '.width'
1220            if value == QtCore.Qt.Checked:
1221                self.poly_params_to_fit.append(parameter_name)
1222            else:
1223                if parameter_name in self.poly_params_to_fit:
1224                    self.poly_params_to_fit.remove(parameter_name)
1225            self.cmdFit.setEnabled(self.haveParamsToFit())
1226
1227        elif model_column in [delegate.poly_min, delegate.poly_max]:
1228            try:
1229                value = GuiUtils.toDouble(item.text())
1230            except TypeError:
1231                # Can't be converted properly, bring back the old value and exit
1232                return
1233
1234            current_details = self.kernel_module.details[parameter_name]
1235            if self.has_poly_error_column:
1236                # err column changes the indexing
1237                current_details[model_column-2] = value
1238            else:
1239                current_details[model_column-1] = value
1240
1241        elif model_column == delegate.poly_function:
1242            # name of the function - just pass
1243            pass
1244
1245        else:
1246            try:
1247                value = GuiUtils.toDouble(item.text())
1248            except TypeError:
1249                # Can't be converted properly, bring back the old value and exit
1250                return
1251
1252            # Update the sasmodel
1253            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
1254            #self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1255            key = parameter_name + '.' + delegate.columnDict()[model_column]
1256            self.poly_params[key] = value
1257
1258            # Update plot
1259            self.updateData()
1260
1261        # update in param model
1262        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1263            row = self.getRowFromName(parameter_name)
1264            param_item = self._model_model.item(row).child(0).child(0, model_column)
1265            if param_item is None:
1266                return
1267            self._model_model.blockSignals(True)
1268            param_item.setText(item.text())
1269            self._model_model.blockSignals(False)
1270
1271    def onMagnetModelChange(self, item):
1272        """
1273        Callback method for updating the sasmodel magnetic parameters with the GUI values
1274        """
1275        model_column = item.column()
1276        model_row = item.row()
1277        name_index = self._magnet_model.index(model_row, 0)
1278        parameter_name = str(self._magnet_model.data(name_index))
1279
1280        if model_column == 0:
1281            value = item.checkState()
1282            if value == QtCore.Qt.Checked:
1283                self.magnet_params_to_fit.append(parameter_name)
1284            else:
1285                if parameter_name in self.magnet_params_to_fit:
1286                    self.magnet_params_to_fit.remove(parameter_name)
1287            self.cmdFit.setEnabled(self.haveParamsToFit())
1288            # Update state stack
1289            self.updateUndo()
1290            return
1291
1292        # Extract changed value.
1293        try:
1294            value = GuiUtils.toDouble(item.text())
1295        except TypeError:
1296            # Unparsable field
1297            return
1298        delegate = self.lstMagnetic.itemDelegate()
1299
1300        if model_column > 1:
1301            if model_column == delegate.mag_min:
1302                pos = 1
1303            elif model_column == delegate.mag_max:
1304                pos = 2
1305            elif model_column == delegate.mag_unit:
1306                pos = 0
1307            else:
1308                raise AttributeError("Wrong column in magnetism table.")
1309            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1310            self.kernel_module.details[parameter_name][pos] = value
1311        else:
1312            self.magnet_params[parameter_name] = value
1313            #self.kernel_module.setParam(parameter_name) = value
1314            # Force the chart update when actual parameters changed
1315            self.recalculatePlotData()
1316
1317        # Update state stack
1318        self.updateUndo()
1319
1320    def onHelp(self):
1321        """
1322        Show the "Fitting" section of help
1323        """
1324        tree_location = "/user/qtgui/Perspectives/Fitting/"
1325
1326        # Actual file will depend on the current tab
1327        tab_id = self.tabFitting.currentIndex()
1328        helpfile = "fitting.html"
1329        if tab_id == 0:
1330            helpfile = "fitting_help.html"
1331        elif tab_id == 1:
1332            helpfile = "residuals_help.html"
1333        elif tab_id == 2:
1334            helpfile = "resolution.html"
1335        elif tab_id == 3:
1336            helpfile = "pd/polydispersity.html"
1337        elif tab_id == 4:
1338            helpfile = "magnetism/magnetism.html"
1339        help_location = tree_location + helpfile
1340
1341        self.showHelp(help_location)
1342
1343    def showHelp(self, url):
1344        """
1345        Calls parent's method for opening an HTML page
1346        """
1347        self.parent.showHelp(url)
1348
1349    def onDisplayMagneticAngles(self):
1350        """
1351        Display a simple image showing direction of magnetic angles
1352        """
1353        self.magneticAnglesWidget.show()
1354
1355    def onFit(self):
1356        """
1357        Perform fitting on the current data
1358        """
1359        if self.fit_started:
1360            self.stopFit()
1361            return
1362
1363        # initialize fitter constants
1364        fit_id = 0
1365        handler = None
1366        batch_inputs = {}
1367        batch_outputs = {}
1368        #---------------------------------
1369        if LocalConfig.USING_TWISTED:
1370            handler = None
1371            updater = None
1372        else:
1373            handler = ConsoleUpdate(parent=self.parent,
1374                                    manager=self,
1375                                    improvement_delta=0.1)
1376            updater = handler.update_fit
1377
1378        # Prepare the fitter object
1379        try:
1380            fitters, _ = self.prepareFitters()
1381        except ValueError as ex:
1382            # This should not happen! GUI explicitly forbids this situation
1383            self.communicate.statusBarUpdateSignal.emit(str(ex))
1384            return
1385
1386        # keep local copy of kernel parameters, as they will change during the update
1387        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1388
1389        # Create the fitting thread, based on the fitter
1390        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1391
1392        self.calc_fit = FitThread(handler=handler,
1393                            fn=fitters,
1394                            batch_inputs=batch_inputs,
1395                            batch_outputs=batch_outputs,
1396                            page_id=[[self.page_id]],
1397                            updatefn=updater,
1398                            completefn=completefn,
1399                            reset_flag=self.is_chain_fitting)
1400
1401        if LocalConfig.USING_TWISTED:
1402            # start the trhrhread with twisted
1403            calc_thread = threads.deferToThread(self.calc_fit.compute)
1404            calc_thread.addCallback(completefn)
1405            calc_thread.addErrback(self.fitFailed)
1406        else:
1407            # Use the old python threads + Queue
1408            self.calc_fit.queue()
1409            self.calc_fit.ready(2.5)
1410
1411        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1412        self.fit_started = True
1413
1414        # Disable some elements
1415        self.disableInteractiveElements()
1416
1417    def stopFit(self):
1418        """
1419        Attempt to stop the fitting thread
1420        """
1421        if self.calc_fit is None or not self.calc_fit.isrunning():
1422            return
1423        self.calc_fit.stop()
1424        #re-enable the Fit button
1425        self.enableInteractiveElements()
1426
1427        msg = "Fitting cancelled."
1428        self.communicate.statusBarUpdateSignal.emit(msg)
1429
1430    def updateFit(self):
1431        """
1432        """
1433        print("UPDATE FIT")
1434        pass
1435
1436    def fitFailed(self, reason):
1437        """
1438        """
1439        self.enableInteractiveElements()
1440        msg = "Fitting failed with: "+ str(reason)
1441        self.communicate.statusBarUpdateSignal.emit(msg)
1442
1443    def batchFittingCompleted(self, result):
1444        """
1445        Send the finish message from calculate threads to main thread
1446        """
1447        if result is None:
1448            result = tuple()
1449        self.batchFittingFinishedSignal.emit(result)
1450
1451    def batchFitComplete(self, result):
1452        """
1453        Receive and display batch fitting results
1454        """
1455        #re-enable the Fit button
1456        self.enableInteractiveElements()
1457
1458        if len(result) == 0:
1459            msg = "Fitting failed."
1460            self.communicate.statusBarUpdateSignal.emit(msg)
1461            return
1462
1463        # Show the grid panel
1464        self.communicate.sendDataToGridSignal.emit(result[0])
1465
1466        elapsed = result[1]
1467        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1468        self.communicate.statusBarUpdateSignal.emit(msg)
1469
1470        # Run over the list of results and update the items
1471        for res_index, res_list in enumerate(result[0]):
1472            # results
1473            res = res_list[0]
1474            param_dict = self.paramDictFromResults(res)
1475
1476            # create local kernel_module
1477            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1478            # pull out current data
1479            data = self._logic[res_index].data
1480
1481            # Switch indexes
1482            self.onSelectBatchFilename(res_index)
1483
1484            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1485            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1486
1487        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1488        if self.kernel_module is not None:
1489            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1490
1491    def paramDictFromResults(self, results):
1492        """
1493        Given the fit results structure, pull out optimized parameters and return them as nicely
1494        formatted dict
1495        """
1496        if results.fitness is None or \
1497            not np.isfinite(results.fitness) or \
1498            np.any(results.pvec is None) or \
1499            not np.all(np.isfinite(results.pvec)):
1500            msg = "Fitting did not converge!"
1501            self.communicate.statusBarUpdateSignal.emit(msg)
1502            msg += results.mesg
1503            logger.error(msg)
1504            return
1505
1506        param_list = results.param_list # ['radius', 'radius.width']
1507        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1508        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1509        params_and_errors = list(zip(param_values, param_stderr))
1510        param_dict = dict(zip(param_list, params_and_errors))
1511
1512        return param_dict
1513
1514    def fittingCompleted(self, result):
1515        """
1516        Send the finish message from calculate threads to main thread
1517        """
1518        if result is None:
1519            result = tuple()
1520        self.fittingFinishedSignal.emit(result)
1521
1522    def fitComplete(self, result):
1523        """
1524        Receive and display fitting results
1525        "result" is a tuple of actual result list and the fit time in seconds
1526        """
1527        #re-enable the Fit button
1528        self.enableInteractiveElements()
1529
1530        if len(result) == 0:
1531            msg = "Fitting failed."
1532            self.communicate.statusBarUpdateSignal.emit(msg)
1533            return
1534
1535        res_list = result[0][0]
1536        res = res_list[0]
1537        self.chi2 = res.fitness
1538        param_dict = self.paramDictFromResults(res)
1539
1540        if param_dict is None:
1541            return
1542
1543        elapsed = result[1]
1544        if self.calc_fit is not None and self.calc_fit._interrupting:
1545            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1546            logger.warning("\n"+msg+"\n")
1547        else:
1548            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1549        self.communicate.statusBarUpdateSignal.emit(msg)
1550
1551        # Dictionary of fitted parameter: value, error
1552        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1553        self.updateModelFromList(param_dict)
1554
1555        self.updatePolyModelFromList(param_dict)
1556
1557        self.updateMagnetModelFromList(param_dict)
1558
1559        # update charts
1560        self.onPlot()
1561        #self.recalculatePlotData()
1562
1563
1564        # Read only value - we can get away by just printing it here
1565        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1566        self.lblChi2Value.setText(chi2_repr)
1567
1568    def prepareFitters(self, fitter=None, fit_id=0):
1569        """
1570        Prepare the Fitter object for use in fitting
1571        """
1572        # fitter = None -> single/batch fitting
1573        # fitter = Fit() -> simultaneous fitting
1574
1575        # Data going in
1576        data = self.logic.data
1577        model = copy.deepcopy(self.kernel_module)
1578        qmin = self.q_range_min
1579        qmax = self.q_range_max
1580        # add polydisperse/magnet parameters if asked
1581        self.updateKernelModelWithExtraParams(model)
1582
1583        params_to_fit = copy.deepcopy(self.main_params_to_fit)
1584        if self.chkPolydispersity.isChecked():
1585            params_to_fit += self.poly_params_to_fit
1586        if self.chkMagnetism.isChecked():
1587            params_to_fit += self.magnet_params_to_fit
1588        if not params_to_fit:
1589            raise ValueError('Fitting requires at least one parameter to optimize.')
1590
1591        # Get the constraints.
1592        constraints = self.getComplexConstraintsForModel()
1593        if fitter is None:
1594            # For single fits - check for inter-model constraints
1595            constraints = self.getConstraintsForFitting()
1596
1597        smearer = self.smearing_widget.smearer()
1598        handler = None
1599        batch_inputs = {}
1600        batch_outputs = {}
1601
1602        fitters = []
1603        for fit_index in self.all_data:
1604            fitter_single = Fit() if fitter is None else fitter
1605            data = GuiUtils.dataFromItem(fit_index)
1606            # Potential weights added directly to data
1607            weighted_data = self.addWeightingToData(data)
1608            try:
1609                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1610                             constraints=constraints)
1611            except ValueError as ex:
1612                raise ValueError("Setting model parameters failed with: %s" % ex)
1613
1614            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1615            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1616                            qmax=qmax)
1617            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1618            if fitter is None:
1619                # Assign id to the new fitter only
1620                fitter_single.fitter_id = [self.page_id]
1621            fit_id += 1
1622            fitters.append(fitter_single)
1623
1624        return fitters, fit_id
1625
1626    def iterateOverModel(self, func):
1627        """
1628        Take func and throw it inside the model row loop
1629        """
1630        for row_i in range(self._model_model.rowCount()):
1631            func(row_i)
1632
1633    def updateModelFromList(self, param_dict):
1634        """
1635        Update the model with new parameters, create the errors column
1636        """
1637        assert isinstance(param_dict, dict)
1638        if not dict:
1639            return
1640
1641        def updateFittedValues(row):
1642            # Utility function for main model update
1643            # internal so can use closure for param_dict
1644            param_name = str(self._model_model.item(row, 0).text())
1645            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1646                return
1647            # modify the param value
1648            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1649            self._model_model.item(row, 1).setText(param_repr)
1650            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1651            if self.has_error_column:
1652                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1653                self._model_model.item(row, 2).setText(error_repr)
1654
1655        def updatePolyValues(row):
1656            # Utility function for updateof polydispersity part of the main model
1657            param_name = str(self._model_model.item(row, 0).text())+'.width'
1658            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1659                return
1660            # modify the param value
1661            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1662            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1663            # modify the param error
1664            if self.has_error_column:
1665                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1666                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1667
1668        def createErrorColumn(row):
1669            # Utility function for error column update
1670            item = QtGui.QStandardItem()
1671            def createItem(param_name):
1672                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1673                item.setText(error_repr)
1674            def curr_param():
1675                return str(self._model_model.item(row, 0).text())
1676
1677            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1678
1679            error_column.append(item)
1680
1681        def createPolyErrorColumn(row):
1682            # Utility function for error column update in the polydispersity sub-rows
1683            # NOTE: only creates empty items; updatePolyValues adds the error value
1684            item = self._model_model.item(row, 0)
1685            if not item.hasChildren():
1686                return
1687            poly_item = item.child(0)
1688            if not poly_item.hasChildren():
1689                return
1690            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1691
1692        if not self.has_error_column:
1693            # create top-level error column
1694            error_column = []
1695            self.lstParams.itemDelegate().addErrorColumn()
1696            self.iterateOverModel(createErrorColumn)
1697
1698            self._model_model.insertColumn(2, error_column)
1699
1700            FittingUtilities.addErrorHeadersToModel(self._model_model)
1701
1702            # create error column in polydispersity sub-rows
1703            self.iterateOverModel(createPolyErrorColumn)
1704
1705            self.has_error_column = True
1706
1707        # block signals temporarily, so we don't end up
1708        # updating charts with every single model change on the end of fitting
1709        self._model_model.itemChanged.disconnect()
1710        self.iterateOverModel(updateFittedValues)
1711        self.iterateOverModel(updatePolyValues)
1712        self._model_model.itemChanged.connect(self.onMainParamsChange)
1713
1714    def iterateOverPolyModel(self, func):
1715        """
1716        Take func and throw it inside the poly model row loop
1717        """
1718        for row_i in range(self._poly_model.rowCount()):
1719            func(row_i)
1720
1721    def updatePolyModelFromList(self, param_dict):
1722        """
1723        Update the polydispersity model with new parameters, create the errors column
1724        """
1725        assert isinstance(param_dict, dict)
1726        if not dict:
1727            return
1728
1729        def updateFittedValues(row_i):
1730            # Utility function for main model update
1731            # internal so can use closure for param_dict
1732            if row_i >= self._poly_model.rowCount():
1733                return
1734            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1735            if param_name not in list(param_dict.keys()):
1736                return
1737            # modify the param value
1738            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1739            self._poly_model.item(row_i, 1).setText(param_repr)
1740            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1741            if self.has_poly_error_column:
1742                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1743                self._poly_model.item(row_i, 2).setText(error_repr)
1744
1745        def createErrorColumn(row_i):
1746            # Utility function for error column update
1747            if row_i >= self._poly_model.rowCount():
1748                return
1749            item = QtGui.QStandardItem()
1750
1751            def createItem(param_name):
1752                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1753                item.setText(error_repr)
1754
1755            def poly_param():
1756                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1757
1758            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1759
1760            error_column.append(item)
1761
1762        # block signals temporarily, so we don't end up
1763        # updating charts with every single model change on the end of fitting
1764        self._poly_model.itemChanged.disconnect()
1765        self.iterateOverPolyModel(updateFittedValues)
1766        self._poly_model.itemChanged.connect(self.onPolyModelChange)
1767
1768        if self.has_poly_error_column:
1769            return
1770
1771        self.lstPoly.itemDelegate().addErrorColumn()
1772        error_column = []
1773        self.iterateOverPolyModel(createErrorColumn)
1774
1775        # switch off reponse to model change
1776        self._poly_model.insertColumn(2, error_column)
1777        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1778
1779        self.has_poly_error_column = True
1780
1781    def iterateOverMagnetModel(self, func):
1782        """
1783        Take func and throw it inside the magnet model row loop
1784        """
1785        for row_i in range(self._magnet_model.rowCount()):
1786            func(row_i)
1787
1788    def updateMagnetModelFromList(self, param_dict):
1789        """
1790        Update the magnetic model with new parameters, create the errors column
1791        """
1792        assert isinstance(param_dict, dict)
1793        if not dict:
1794            return
1795        if self._magnet_model.rowCount() == 0:
1796            return
1797
1798        def updateFittedValues(row):
1799            # Utility function for main model update
1800            # internal so can use closure for param_dict
1801            if self._magnet_model.item(row, 0) is None:
1802                return
1803            param_name = str(self._magnet_model.item(row, 0).text())
1804            if param_name not in list(param_dict.keys()):
1805                return
1806            # modify the param value
1807            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1808            self._magnet_model.item(row, 1).setText(param_repr)
1809            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1810            if self.has_magnet_error_column:
1811                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1812                self._magnet_model.item(row, 2).setText(error_repr)
1813
1814        def createErrorColumn(row):
1815            # Utility function for error column update
1816            item = QtGui.QStandardItem()
1817            def createItem(param_name):
1818                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1819                item.setText(error_repr)
1820            def curr_param():
1821                return str(self._magnet_model.item(row, 0).text())
1822
1823            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1824
1825            error_column.append(item)
1826
1827        # block signals temporarily, so we don't end up
1828        # updating charts with every single model change on the end of fitting
1829        self._magnet_model.itemChanged.disconnect()
1830        self.iterateOverMagnetModel(updateFittedValues)
1831        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
1832
1833        if self.has_magnet_error_column:
1834            return
1835
1836        self.lstMagnetic.itemDelegate().addErrorColumn()
1837        error_column = []
1838        self.iterateOverMagnetModel(createErrorColumn)
1839
1840        # switch off reponse to model change
1841        self._magnet_model.insertColumn(2, error_column)
1842        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1843
1844        self.has_magnet_error_column = True
1845
1846    def onPlot(self):
1847        """
1848        Plot the current set of data
1849        """
1850        # Regardless of previous state, this should now be `plot show` functionality only
1851        self.cmdPlot.setText("Show Plot")
1852        # Force data recalculation so existing charts are updated
1853        if not self.data_is_loaded:
1854            self.showTheoryPlot()
1855        else:
1856            self.showPlot()
1857        # This is an important processEvent.
1858        # This allows charts to be properly updated in order
1859        # of plots being applied.
1860        QtWidgets.QApplication.processEvents()
1861        self.recalculatePlotData() # recalc+plot theory again (2nd)
1862
1863    def onSmearingOptionsUpdate(self):
1864        """
1865        React to changes in the smearing widget
1866        """
1867        self.calculateQGridForModel()
1868
1869    def recalculatePlotData(self):
1870        """
1871        Generate a new dataset for model
1872        """
1873        if not self.data_is_loaded:
1874            self.createDefaultDataset()
1875        self.calculateQGridForModel()
1876
1877    def showTheoryPlot(self):
1878        """
1879        Show the current theory plot in MPL
1880        """
1881        # Show the chart if ready
1882        if self.theory_item is None:
1883            self.recalculatePlotData()
1884        elif self.model_data:
1885            self._requestPlots(self.model_data.filename, self.theory_item.model())
1886
1887    def showPlot(self):
1888        """
1889        Show the current plot in MPL
1890        """
1891        # Show the chart if ready
1892        data_to_show = self.data
1893        # Any models for this page
1894        current_index = self.all_data[self.data_index]
1895        item = self._requestPlots(self.data.filename, current_index.model())
1896        if item:
1897            # fit+data has not been shown - show just data
1898            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
1899
1900    def _requestPlots(self, item_name, item_model):
1901        """
1902        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
1903        """
1904        fitpage_name = "" if self.tab_id is None else "M"+str(self.tab_id)
1905        plots = GuiUtils.plotsFromFilename(item_name, item_model)
1906        # Has the fitted data been shown?
1907        data_shown = False
1908        item = None
1909        for item, plot in plots.items():
1910            if fitpage_name in plot.name:
1911                data_shown = True
1912                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1913        # return the last data item seen, if nothing was plotted; supposed to be just data)
1914        return None if data_shown else item
1915
1916    def onOptionsUpdate(self):
1917        """
1918        Update local option values and replot
1919        """
1920        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1921            self.options_widget.state()
1922        # set Q range labels on the main tab
1923        self.lblMinRangeDef.setText(str(self.q_range_min))
1924        self.lblMaxRangeDef.setText(str(self.q_range_max))
1925        self.recalculatePlotData()
1926
1927    def setDefaultStructureCombo(self):
1928        """
1929        Fill in the structure factors combo box with defaults
1930        """
1931        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1932        factors = [factor[0] for factor in structure_factor_list]
1933        factors.insert(0, STRUCTURE_DEFAULT)
1934        self.cbStructureFactor.clear()
1935        self.cbStructureFactor.addItems(sorted(factors))
1936
1937    def createDefaultDataset(self):
1938        """
1939        Generate default Dataset 1D/2D for the given model
1940        """
1941        # Create default datasets if no data passed
1942        if self.is2D:
1943            qmax = self.q_range_max/np.sqrt(2)
1944            qstep = self.npts
1945            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1946            return
1947        elif self.log_points:
1948            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1949            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1950            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1951        else:
1952            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1953                                   num=self.npts, endpoint=True)
1954        self.logic.createDefault1dData(interval, self.tab_id)
1955
1956    def readCategoryInfo(self):
1957        """
1958        Reads the categories in from file
1959        """
1960        self.master_category_dict = defaultdict(list)
1961        self.by_model_dict = defaultdict(list)
1962        self.model_enabled_dict = defaultdict(bool)
1963
1964        categorization_file = CategoryInstaller.get_user_file()
1965        if not os.path.isfile(categorization_file):
1966            categorization_file = CategoryInstaller.get_default_file()
1967        with open(categorization_file, 'rb') as cat_file:
1968            self.master_category_dict = json.load(cat_file)
1969            self.regenerateModelDict()
1970
1971        # Load the model dict
1972        models = load_standard_models()
1973        for model in models:
1974            self.models[model.name] = model
1975
1976        self.readCustomCategoryInfo()
1977
1978    def readCustomCategoryInfo(self):
1979        """
1980        Reads the custom model category
1981        """
1982        #Looking for plugins
1983        self.plugins = list(self.custom_models.values())
1984        plugin_list = []
1985        for name, plug in self.custom_models.items():
1986            self.models[name] = plug
1987            plugin_list.append([name, True])
1988        if plugin_list:
1989            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1990
1991    def regenerateModelDict(self):
1992        """
1993        Regenerates self.by_model_dict which has each model name as the
1994        key and the list of categories belonging to that model
1995        along with the enabled mapping
1996        """
1997        self.by_model_dict = defaultdict(list)
1998        for category in self.master_category_dict:
1999            for (model, enabled) in self.master_category_dict[category]:
2000                self.by_model_dict[model].append(category)
2001                self.model_enabled_dict[model] = enabled
2002
2003    def addBackgroundToModel(self, model):
2004        """
2005        Adds background parameter with default values to the model
2006        """
2007        assert isinstance(model, QtGui.QStandardItemModel)
2008        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2009        FittingUtilities.addCheckedListToModel(model, checked_list)
2010        last_row = model.rowCount()-1
2011        model.item(last_row, 0).setEditable(False)
2012        model.item(last_row, 4).setEditable(False)
2013
2014    def addScaleToModel(self, model):
2015        """
2016        Adds scale parameter with default values to the model
2017        """
2018        assert isinstance(model, QtGui.QStandardItemModel)
2019        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2020        FittingUtilities.addCheckedListToModel(model, checked_list)
2021        last_row = model.rowCount()-1
2022        model.item(last_row, 0).setEditable(False)
2023        model.item(last_row, 4).setEditable(False)
2024
2025    def addWeightingToData(self, data):
2026        """
2027        Adds weighting contribution to fitting data
2028        """
2029        new_data = copy.deepcopy(data)
2030        # Send original data for weighting
2031        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2032        if self.is2D:
2033            new_data.err_data = weight
2034        else:
2035            new_data.dy = weight
2036
2037        return new_data
2038
2039    def updateQRange(self):
2040        """
2041        Updates Q Range display
2042        """
2043        if self.data_is_loaded:
2044            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2045        # set Q range labels on the main tab
2046        self.lblMinRangeDef.setText(str(self.q_range_min))
2047        self.lblMaxRangeDef.setText(str(self.q_range_max))
2048        # set Q range labels on the options tab
2049        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2050
2051    def SASModelToQModel(self, model_name, structure_factor=None):
2052        """
2053        Setting model parameters into table based on selected category
2054        """
2055        # Crete/overwrite model items
2056        self._model_model.clear()
2057        self._poly_model.clear()
2058        self._magnet_model.clear()
2059
2060        if model_name is None:
2061            if structure_factor not in (None, "None"):
2062                # S(Q) on its own, treat the same as a form factor
2063                self.kernel_module = None
2064                self.fromStructureFactorToQModel(structure_factor)
2065            else:
2066                # No models selected
2067                return
2068        else:
2069            self.fromModelToQModel(model_name)
2070            self.addExtraShells()
2071
2072            # Allow the SF combobox visibility for the given sasmodel
2073            self.enableStructureFactorControl(structure_factor)
2074       
2075            # Add S(Q)
2076            if self.cbStructureFactor.isEnabled():
2077                structure_factor = self.cbStructureFactor.currentText()
2078                self.fromStructureFactorToQModel(structure_factor)
2079
2080            # Add polydispersity to the model
2081            self.poly_params = {}
2082            self.setPolyModel()
2083            # Add magnetic parameters to the model
2084            self.magnet_params = {}
2085            self.setMagneticModel()
2086
2087        # Now we claim the model has been loaded
2088        self.model_is_loaded = True
2089        # Change the model name to a monicker
2090        self.kernel_module.name = self.modelName()
2091        # Update the smearing tab
2092        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2093
2094        # (Re)-create headers
2095        FittingUtilities.addHeadersToModel(self._model_model)
2096        self.lstParams.header().setFont(self.boldFont)
2097
2098        # Update Q Ranges
2099        self.updateQRange()
2100
2101    def fromModelToQModel(self, model_name):
2102        """
2103        Setting model parameters into QStandardItemModel based on selected _model_
2104        """
2105        name = model_name
2106        kernel_module = None
2107        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2108            # custom kernel load requires full path
2109            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2110        try:
2111            kernel_module = generate.load_kernel_module(name)
2112        except ModuleNotFoundError as ex:
2113            pass
2114        except FileNotFoundError as ex:
2115            # can happen when name attribute not the same as actual filename
2116            pass
2117
2118        if kernel_module is None:
2119            # mismatch between "name" attribute and actual filename.
2120            curr_model = self.models[model_name]
2121            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2122            try:
2123                kernel_module = generate.load_kernel_module(name)
2124            except ModuleNotFoundError as ex:
2125                logger.error("Can't find the model "+ str(ex))
2126                return
2127
2128        if hasattr(kernel_module, 'parameters'):
2129            # built-in and custom models
2130            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2131
2132        elif hasattr(kernel_module, 'model_info'):
2133            # for sum/multiply models
2134            self.model_parameters = kernel_module.model_info.parameters
2135
2136        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2137            # this probably won't work if there's no model_info, but just in case
2138            self.model_parameters = kernel_module.Model._model_info.parameters
2139        else:
2140            # no parameters - default to blank table
2141            msg = "No parameters found in model '{}'.".format(model_name)
2142            logger.warning(msg)
2143            self.model_parameters = modelinfo.ParameterTable([])
2144
2145        # Instantiate the current sasmodel
2146        self.kernel_module = self.models[model_name]()
2147
2148        # Change the model name to a monicker
2149        self.kernel_module.name = self.modelName()
2150
2151        # Explicitly add scale and background with default values
2152        temp_undo_state = self.undo_supported
2153        self.undo_supported = False
2154        self.addScaleToModel(self._model_model)
2155        self.addBackgroundToModel(self._model_model)
2156        self.undo_supported = temp_undo_state
2157
2158        self.shell_names = self.shellNamesList()
2159
2160        # Add heading row
2161        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2162
2163        # Update the QModel
2164        FittingUtilities.addParametersToModel(
2165                self.model_parameters,
2166                self.kernel_module,
2167                self.is2D,
2168                self._model_model,
2169                self.lstParams)
2170
2171    def fromStructureFactorToQModel(self, structure_factor):
2172        """
2173        Setting model parameters into QStandardItemModel based on selected _structure factor_
2174        """
2175        if structure_factor is None or structure_factor=="None":
2176            return
2177
2178        product_params = None
2179
2180        if self.kernel_module is None:
2181            # Structure factor is the only selected model; build it and show all its params
2182            self.kernel_module = self.models[structure_factor]()
2183            self.kernel_module.name = self.modelName()
2184            s_params = self.kernel_module._model_info.parameters
2185            s_params_orig = s_params
2186        else:
2187            s_kernel = self.models[structure_factor]()
2188            p_kernel = self.kernel_module
2189            # need to reset multiplicity to get the right product
2190            if p_kernel.is_multiplicity_model:
2191                p_kernel.multiplicity = p_kernel.multiplicity_info.number
2192
2193            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2194            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2195
2196            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2197            # Modify the name to correspond to shown items
2198            self.kernel_module.name = self.modelName()
2199            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2200            all_param_names = [param.name for param in all_params]
2201
2202            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2203            # conflicting names with P(Q) params will cause a rename
2204
2205            if "radius_effective_mode" in all_param_names:
2206                # Show all parameters
2207                # In this case, radius_effective is NOT pruned by sasmodels.product
2208                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2209                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2210                product_params = modelinfo.ParameterTable(
2211                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2212            else:
2213                # Ensure radius_effective is not displayed
2214                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2215                if "radius_effective" in all_param_names:
2216                    # In this case, radius_effective is NOT pruned by sasmodels.product
2217                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2218                    product_params = modelinfo.ParameterTable(
2219                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2220                else:
2221                    # In this case, radius_effective is pruned by sasmodels.product
2222                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2223                    product_params = modelinfo.ParameterTable(
2224                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2225
2226        # Add heading row
2227        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2228
2229        # Get new rows for QModel
2230        # Any renamed parameters are stored as data in the relevant item, for later handling
2231        FittingUtilities.addSimpleParametersToModel(
2232                parameters=s_params,
2233                is2D=self.is2D,
2234                parameters_original=s_params_orig,
2235                model=self._model_model,
2236                view=self.lstParams)
2237
2238        # Insert product-only params into QModel
2239        if product_params:
2240            prod_rows = FittingUtilities.addSimpleParametersToModel(
2241                    parameters=product_params,
2242                    is2D=self.is2D,
2243                    parameters_original=None,
2244                    model=self._model_model,
2245                    view=self.lstParams,
2246                    row_num=2)
2247
2248            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2249            self._n_shells_row += len(prod_rows)
2250
2251    def haveParamsToFit(self):
2252        """
2253        Finds out if there are any parameters ready to be fitted
2254        """
2255        if not self.logic.data_is_loaded:
2256            return False
2257        if self.main_params_to_fit:
2258            return True
2259        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2260            return True
2261        if self.chkMagnetism.isChecked() and self.magnet_params_to_fit:
2262            return True
2263        return False
2264
2265    def onMainParamsChange(self, item):
2266        """
2267        Callback method for updating the sasmodel parameters with the GUI values
2268        """
2269        model_column = item.column()
2270
2271        if model_column == 0:
2272            self.checkboxSelected(item)
2273            self.cmdFit.setEnabled(self.haveParamsToFit())
2274            # Update state stack
2275            self.updateUndo()
2276            return
2277
2278        model_row = item.row()
2279        name_index = self._model_model.index(model_row, 0)
2280        name_item = self._model_model.itemFromIndex(name_index)
2281
2282        # Extract changed value.
2283        try:
2284            value = GuiUtils.toDouble(item.text())
2285        except TypeError:
2286            # Unparsable field
2287            return
2288
2289        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2290        if name_item.data(QtCore.Qt.UserRole):
2291            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2292        else:
2293            parameter_name = str(self._model_model.data(name_index))
2294
2295        # Update the parameter value - note: this supports +/-inf as well
2296        self.kernel_module.params[parameter_name] = value
2297
2298        # Update the parameter value - note: this supports +/-inf as well
2299        param_column = self.lstParams.itemDelegate().param_value
2300        min_column = self.lstParams.itemDelegate().param_min
2301        max_column = self.lstParams.itemDelegate().param_max
2302        if model_column == param_column:
2303            self.kernel_module.setParam(parameter_name, value)
2304        elif model_column == min_column:
2305            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2306            self.kernel_module.details[parameter_name][1] = value
2307        elif model_column == max_column:
2308            self.kernel_module.details[parameter_name][2] = value
2309        else:
2310            # don't update the chart
2311            return
2312
2313        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2314        # TODO: multishell params in self.kernel_module.details[??] = value
2315
2316        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2317        # necessary
2318        self.processEffectiveRadius()
2319
2320        # Force the chart update when actual parameters changed
2321        if model_column == 1:
2322            self.recalculatePlotData()
2323
2324        # Update state stack
2325        self.updateUndo()
2326
2327    def processEffectiveRadius(self):
2328        """
2329        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2330        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2331        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2332
2333        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2334        displayed for the user; that is not dealt with here, but in complete1D.
2335        """
2336        ER_row = self.getRowFromName("radius_effective")
2337        if ER_row is None:
2338            return
2339
2340        ER_mode_row = self.getRowFromName("radius_effective_mode")
2341        if ER_mode_row is None:
2342            return
2343
2344        try:
2345            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2346        except ValueError:
2347            logging.error("radius_effective_mode was set to an invalid value.")
2348            return
2349
2350        if ER_mode == 0:
2351            # ensure the ER value can be modified by user
2352            self.setParamEditableByRow(ER_row, True)
2353        elif ER_mode > 0:
2354            # ensure the ER value cannot be modified by user
2355            self.setParamEditableByRow(ER_row, False)
2356        else:
2357            logging.error("radius_effective_mode was set to an invalid value.")
2358
2359    def setParamEditableByRow(self, row, editable=True):
2360        """
2361        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2362        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2363        """
2364        item_name = self._model_model.item(row, 0)
2365        item_value = self._model_model.item(row, 1)
2366
2367        item_value.setEditable(editable)
2368
2369        if editable:
2370            # reset font
2371            item_name.setFont(QtGui.QFont())
2372            # reset colour
2373            item_name.setForeground(QtGui.QBrush())
2374            # make checkable
2375            item_name.setCheckable(True)
2376        else:
2377            # change font
2378            font = QtGui.QFont()
2379            font.setItalic(True)
2380            item_name.setFont(font)
2381            # change colour
2382            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2383            # make not checkable (and uncheck)
2384            item_name.setCheckState(QtCore.Qt.Unchecked)
2385            item_name.setCheckable(False)
2386
2387    def isCheckable(self, row):
2388        return self._model_model.item(row, 0).isCheckable()
2389
2390    def checkboxSelected(self, item):
2391        # Assure we're dealing with checkboxes
2392        if not item.isCheckable():
2393            return
2394        status = item.checkState()
2395
2396        # If multiple rows selected - toggle all of them, filtering uncheckable
2397        # Switch off signaling from the model to avoid recursion
2398        self._model_model.blockSignals(True)
2399        # Convert to proper indices and set requested enablement
2400        self.setParameterSelection(status)
2401        self._model_model.blockSignals(False)
2402
2403        # update the list of parameters to fit
2404        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2405
2406    def checkedListFromModel(self, model):
2407        """
2408        Returns list of checked parameters for given model
2409        """
2410        def isChecked(row):
2411            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2412
2413        return [str(model.item(row_index, 0).text())
2414                for row_index in range(model.rowCount())
2415                if isChecked(row_index)]
2416
2417    def createNewIndex(self, fitted_data):
2418        """
2419        Create a model or theory index with passed Data1D/Data2D
2420        """
2421        if self.data_is_loaded:
2422            if not fitted_data.name:
2423                name = self.nameForFittedData(self.data.filename)
2424                fitted_data.title = name
2425                fitted_data.name = name
2426                fitted_data.filename = name
2427                fitted_data.symbol = "Line"
2428            self.updateModelIndex(fitted_data)
2429        else:
2430            if not fitted_data.name:
2431                name = self.nameForFittedData(self.kernel_module.id)
2432            else:
2433                name = fitted_data.name
2434            fitted_data.title = name
2435            fitted_data.filename = name
2436            fitted_data.symbol = "Line"
2437            self.createTheoryIndex(fitted_data)
2438            # Switch to the theory tab for user's glee
2439            self.communicate.changeDataExplorerTabSignal.emit(1)
2440
2441    def updateModelIndex(self, fitted_data):
2442        """
2443        Update a QStandardModelIndex containing model data
2444        """
2445        name = self.nameFromData(fitted_data)
2446        # Make this a line if no other defined
2447        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2448            fitted_data.symbol = 'Line'
2449        # Notify the GUI manager so it can update the main model in DataExplorer
2450        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2451
2452    def createTheoryIndex(self, fitted_data):
2453        """
2454        Create a QStandardModelIndex containing model data
2455        """
2456        name = self.nameFromData(fitted_data)
2457        # Notify the GUI manager so it can create the theory model in DataExplorer
2458        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2459        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2460
2461    def nameFromData(self, fitted_data):
2462        """
2463        Return name for the dataset. Terribly impure function.
2464        """
2465        if fitted_data.name is None:
2466            name = self.nameForFittedData(self.logic.data.filename)
2467            fitted_data.title = name
2468            fitted_data.name = name
2469            fitted_data.filename = name
2470        else:
2471            name = fitted_data.name
2472        return name
2473
2474    def methodCalculateForData(self):
2475        '''return the method for data calculation'''
2476        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2477
2478    def methodCompleteForData(self):
2479        '''return the method for result parsin on calc complete '''
2480        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2481
2482    def updateKernelModelWithExtraParams(self, model=None):
2483        """
2484        Updates kernel model 'model' with extra parameters from
2485        the polydisp and magnetism tab, if the tabs are enabled
2486        """
2487        if model is None: return
2488        if not hasattr(model, 'setParam'): return
2489
2490        # add polydisperse parameters if asked
2491        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2492            for key, value in self.poly_params.items():
2493                model.setParam(key, value)
2494        # add magnetic params if asked
2495        if self.chkMagnetism.isChecked() and self._magnet_model.rowCount() > 0:
2496            for key, value in self.magnet_params.items():
2497                model.setParam(key, value)
2498
2499    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2500        """
2501        Wrapper for Calc1D/2D calls
2502        """
2503        if data is None:
2504            data = self.data
2505        if model is None:
2506            model = copy.deepcopy(self.kernel_module)
2507            self.updateKernelModelWithExtraParams(model)
2508
2509        if completefn is None:
2510            completefn = self.methodCompleteForData()
2511        smearer = self.smearing_widget.smearer()
2512        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2513
2514        # Disable buttons/table
2515        self.disableInteractiveElements()
2516        # Awful API to a backend method.
2517        calc_thread = self.methodCalculateForData()(data=data,
2518                                               model=model,
2519                                               page_id=0,
2520                                               qmin=self.q_range_min,
2521                                               qmax=self.q_range_max,
2522                                               smearer=smearer,
2523                                               state=None,
2524                                               weight=weight,
2525                                               fid=None,
2526                                               toggle_mode_on=False,
2527                                               completefn=completefn,
2528                                               update_chisqr=True,
2529                                               exception_handler=self.calcException,
2530                                               source=None)
2531        if use_threads:
2532            if LocalConfig.USING_TWISTED:
2533                # start the thread with twisted
2534                thread = threads.deferToThread(calc_thread.compute)
2535                thread.addCallback(completefn)
2536                thread.addErrback(self.calculateDataFailed)
2537            else:
2538                # Use the old python threads + Queue
2539                calc_thread.queue()
2540                calc_thread.ready(2.5)
2541        else:
2542            results = calc_thread.compute()
2543            completefn(results)
2544
2545    def calculateQGridForModel(self):
2546        """
2547        Prepare the fitting data object, based on current ModelModel
2548        """
2549        if self.kernel_module is None:
2550            return
2551        self.calculateQGridForModelExt()
2552
2553    def calculateDataFailed(self, reason):
2554        """
2555        Thread returned error
2556        """
2557        # Bring the GUI to normal state
2558        self.enableInteractiveElements()
2559        print("Calculate Data failed with ", reason)
2560
2561    def completed1D(self, return_data):
2562        self.Calc1DFinishedSignal.emit(return_data)
2563
2564    def completed2D(self, return_data):
2565        self.Calc2DFinishedSignal.emit(return_data)
2566
2567    def complete1D(self, return_data):
2568        """
2569        Plot the current 1D data
2570        """
2571        # Bring the GUI to normal state
2572        self.enableInteractiveElements()
2573        if return_data is None:
2574            return
2575        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2576
2577        # assure the current index is set properly for batch
2578        if len(self._logic) > 1:
2579            for i, logic in enumerate(self._logic):
2580                if logic.data.name in fitted_data.name:
2581                    self.data_index = i
2582
2583        residuals = self.calculateResiduals(fitted_data)
2584        self.model_data = fitted_data
2585        new_plots = [fitted_data]
2586        if residuals is not None:
2587            new_plots.append(residuals)
2588
2589        if self.data_is_loaded:
2590            # delete any plots associated with the data that were not updated
2591            # (e.g. to remove beta(Q), S_eff(Q))
2592            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2593            pass
2594        else:
2595            # delete theory items for the model, in order to get rid of any
2596            # redundant items, e.g. beta(Q), S_eff(Q)
2597            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2598
2599        # Create plots for parameters with enabled polydispersity
2600        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2601            data_id = fitted_data.id.split()
2602            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2603            data_name = fitted_data.name.split()
2604            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2605            self.createNewIndex(plot)
2606            new_plots.append(plot)
2607
2608        # Create plots for intermediate product data
2609        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2610        for plot in plots:
2611            plot.symbol = "Line"
2612            self.createNewIndex(plot)
2613            new_plots.append(plot)
2614
2615        for plot in new_plots:
2616            self.communicate.plotUpdateSignal.emit([plot])
2617
2618        # Update radius_effective if relevant
2619        self.updateEffectiveRadius(return_data)
2620
2621    def complete2D(self, return_data):
2622        """
2623        Plot the current 2D data
2624        """
2625        # Bring the GUI to normal state
2626        self.enableInteractiveElements()
2627
2628        if return_data is None:
2629            return
2630
2631        fitted_data = self.logic.new2DPlot(return_data)
2632        # assure the current index is set properly for batch
2633        if len(self._logic) > 1:
2634            for i, logic in enumerate(self._logic):
2635                if logic.data.name in fitted_data.name:
2636                    self.data_index = i
2637
2638        residuals = self.calculateResiduals(fitted_data)
2639        self.model_data = fitted_data
2640        new_plots = [fitted_data]
2641        if residuals is not None:
2642            new_plots.append(residuals)
2643
2644        # Update/generate plots
2645        for plot in new_plots:
2646            self.communicate.plotUpdateSignal.emit([plot])
2647
2648    def updateEffectiveRadius(self, return_data):
2649        """
2650        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2651        calculated value as returned by sasmodels (if the value was returned).
2652        """
2653        ER_mode_row = self.getRowFromName("radius_effective_mode")
2654        if ER_mode_row is None:
2655            return
2656        try:
2657            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2658        except ValueError:
2659            logging.error("radius_effective_mode was set to an invalid value.")
2660            return
2661        if ER_mode < 1:
2662            # does not need updating if it is not being computed
2663            return
2664
2665        ER_row = self.getRowFromName("radius_effective")
2666        if ER_row is None:
2667            return
2668
2669        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2670        ER_value = scalar_results.get("effective_radius") # note name of key
2671        if ER_value is None:
2672            return
2673        # ensure the model does not recompute when updating the value
2674        self._model_model.blockSignals(True)
2675        self._model_model.item(ER_row, 1).setText(str(ER_value))
2676        self._model_model.blockSignals(False)
2677        # ensure the view is updated immediately
2678        self._model_model.layoutChanged.emit()
2679
2680    def calculateResiduals(self, fitted_data):
2681        """
2682        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2683        """
2684        # Create a new index for holding data
2685        fitted_data.symbol = "Line"
2686
2687        # Modify fitted_data with weighting
2688        weighted_data = self.addWeightingToData(fitted_data)
2689
2690        self.createNewIndex(weighted_data)
2691
2692        # Calculate difference between return_data and logic.data
2693        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2694        # Update the control
2695        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2696        self.lblChi2Value.setText(chi2_repr)
2697
2698        # Plot residuals if actual data
2699        if not self.data_is_loaded:
2700            return
2701
2702        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2703        if residuals_plot is None:
2704            return
2705        residuals_plot.id = "Residual " + residuals_plot.id
2706        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2707        self.createNewIndex(residuals_plot)
2708        return residuals_plot
2709
2710    def onCategoriesChanged(self):
2711            """
2712            Reload the category/model comboboxes
2713            """
2714            # Store the current combo indices
2715            current_cat = self.cbCategory.currentText()
2716            current_model = self.cbModel.currentText()
2717
2718            # reread the category file and repopulate the combo
2719            self.cbCategory.blockSignals(True)
2720            self.cbCategory.clear()
2721            self.readCategoryInfo()
2722            self.initializeCategoryCombo()
2723
2724            # Scroll back to the original index in Categories
2725            new_index = self.cbCategory.findText(current_cat)
2726            if new_index != -1:
2727                self.cbCategory.setCurrentIndex(new_index)
2728            self.cbCategory.blockSignals(False)
2729            # ...and in the Models
2730            self.cbModel.blockSignals(True)
2731            new_index = self.cbModel.findText(current_model)
2732            if new_index != -1:
2733                self.cbModel.setCurrentIndex(new_index)
2734            self.cbModel.blockSignals(False)
2735
2736            return
2737
2738    def calcException(self, etype, value, tb):
2739        """
2740        Thread threw an exception.
2741        """
2742        # Bring the GUI to normal state
2743        self.enableInteractiveElements()
2744        # TODO: remimplement thread cancellation
2745        logger.error("".join(traceback.format_exception(etype, value, tb)))
2746
2747    def setTableProperties(self, table):
2748        """
2749        Setting table properties
2750        """
2751        # Table properties
2752        table.verticalHeader().setVisible(False)
2753        table.setAlternatingRowColors(True)
2754        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2755        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2756        table.resizeColumnsToContents()
2757
2758        # Header
2759        header = table.horizontalHeader()
2760        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2761        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2762
2763        # Qt5: the following 2 lines crash - figure out why!
2764        # Resize column 0 and 7 to content
2765        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2766        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2767
2768    def setPolyModel(self):
2769        """
2770        Set polydispersity values
2771        """
2772        if not self.model_parameters:
2773            return
2774        self._poly_model.clear()
2775
2776        parameters = self.model_parameters.form_volume_parameters
2777        if self.is2D:
2778            parameters += self.model_parameters.orientation_parameters
2779
2780        [self.setPolyModelParameters(i, param) for i, param in \
2781            enumerate(parameters) if param.polydisperse]
2782
2783        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2784
2785    def setPolyModelParameters(self, i, param):
2786        """
2787        Standard of multishell poly parameter driver
2788        """
2789        param_name = param.name
2790        # see it the parameter is multishell
2791        if '[' in param.name:
2792            # Skip empty shells
2793            if self.current_shell_displayed == 0:
2794                return
2795            else:
2796                # Create as many entries as current shells
2797                for ishell in range(1, self.current_shell_displayed+1):
2798                    # Remove [n] and add the shell numeral
2799                    name = param_name[0:param_name.index('[')] + str(ishell)
2800                    self.addNameToPolyModel(i, name)
2801        else:
2802            # Just create a simple param entry
2803            self.addNameToPolyModel(i, param_name)
2804
2805    def addNameToPolyModel(self, i, param_name):
2806        """
2807        Creates a checked row in the poly model with param_name
2808        """
2809        # Polydisp. values from the sasmodel
2810        width = self.kernel_module.getParam(param_name + '.width')
2811        npts = self.kernel_module.getParam(param_name + '.npts')
2812        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2813        _, min, max = self.kernel_module.details[param_name]
2814
2815        # Update local param dict
2816        self.poly_params[param_name + '.width'] = width
2817        self.poly_params[param_name + '.npts'] = npts
2818        self.poly_params[param_name + '.nsigmas'] = nsigs
2819
2820        # Construct a row with polydisp. related variable.
2821        # This will get added to the polydisp. model
2822        # Note: last argument needs extra space padding for decent display of the control
2823        checked_list = ["Distribution of " + param_name, str(width),
2824                        str(min), str(max),
2825                        str(npts), str(nsigs), "gaussian      ",'']
2826        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2827
2828        # All possible polydisp. functions as strings in combobox
2829        func = QtWidgets.QComboBox()
2830        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2831        # Set the default index
2832        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2833        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2834        self.lstPoly.setIndexWidget(ind, func)
2835        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2836
2837    def onPolyFilenameChange(self, row_index):
2838        """
2839        Respond to filename_updated signal from the delegate
2840        """
2841        # For the given row, invoke the "array" combo handler
2842        array_caption = 'array'
2843
2844        # Get the combo box reference
2845        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2846        widget = self.lstPoly.indexWidget(ind)
2847
2848        # Update the combo box so it displays "array"
2849        widget.blockSignals(True)
2850        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2851        widget.blockSignals(False)
2852
2853        # Invoke the file reader
2854        self.onPolyComboIndexChange(array_caption, row_index)
2855
2856    def onPolyComboIndexChange(self, combo_string, row_index):
2857        """
2858        Modify polydisp. defaults on function choice
2859        """
2860        # Get npts/nsigs for current selection
2861        param = self.model_parameters.form_volume_parameters[row_index]
2862        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2863        combo_box = self.lstPoly.indexWidget(file_index)
2864
2865        def updateFunctionCaption(row):
2866            # Utility function for update of polydispersity function name in the main model
2867            if not self.isCheckable(row):
2868                return
2869            self._model_model.blockSignals(True)
2870            param_name = str(self._model_model.item(row, 0).text())
2871            self._model_model.blockSignals(False)
2872            if param_name !=  param.name:
2873                return
2874            # Modify the param value
2875            self._model_model.blockSignals(True)
2876            if self.has_error_column:
2877                # err column changes the indexing
2878                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2879            else:
2880                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2881            self._model_model.blockSignals(False)
2882
2883        if combo_string == 'array':
2884            try:
2885                self.loadPolydispArray(row_index)
2886                # Update main model for display
2887                self.iterateOverModel(updateFunctionCaption)
2888                # disable the row
2889                lo = self.lstPoly.itemDelegate().poly_pd
2890                hi = self.lstPoly.itemDelegate().poly_function
2891                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2892                return
2893            except IOError:
2894                combo_box.setCurrentIndex(self.orig_poly_index)
2895                # Pass for cancel/bad read
2896                pass
2897
2898        # Enable the row in case it was disabled by Array
2899        self._poly_model.blockSignals(True)
2900        max_range = self.lstPoly.itemDelegate().poly_filename
2901        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2902        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2903        self._poly_model.setData(file_index, "")
2904        self._poly_model.blockSignals(False)
2905
2906        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2907        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2908
2909        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2910        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2911
2912        self._poly_model.setData(npts_index, npts)
2913        self._poly_model.setData(nsigs_index, nsigs)
2914
2915        self.iterateOverModel(updateFunctionCaption)
2916        self.orig_poly_index = combo_box.currentIndex()
2917
2918    def loadPolydispArray(self, row_index):
2919        """
2920        Show the load file dialog and loads requested data into state
2921        """
2922        datafile = QtWidgets.QFileDialog.getOpenFileName(
2923            self, "Choose a weight file", "", "All files (*.*)", None,
2924            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2925
2926        if not datafile:
2927            logger.info("No weight data chosen.")
2928            raise IOError
2929
2930        values = []
2931        weights = []
2932        def appendData(data_tuple):
2933            """
2934            Fish out floats from a tuple of strings
2935            """
2936            try:
2937                values.append(float(data_tuple[0]))
2938                weights.append(float(data_tuple[1]))
2939            except (ValueError, IndexError):
2940                # just pass through if line with bad data
2941                return
2942
2943        with open(datafile, 'r') as column_file:
2944            column_data = [line.rstrip().split() for line in column_file.readlines()]
2945            [appendData(line) for line in column_data]
2946
2947        # If everything went well - update the sasmodel values
2948        self.disp_model = POLYDISPERSITY_MODELS['array']()
2949        self.disp_model.set_weights(np.array(values), np.array(weights))
2950        # + update the cell with filename
2951        fname = os.path.basename(str(datafile))
2952        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2953        self._poly_model.setData(fname_index, fname)
2954
2955    def onColumnWidthUpdate(self, index, old_size, new_size):
2956        """
2957        Simple state update of the current column widths in the  param list
2958        """
2959        self.lstParamHeaderSizes[index] = new_size
2960
2961    def setMagneticModel(self):
2962        """
2963        Set magnetism values on model
2964        """
2965        if not self.model_parameters:
2966            return
2967        self._magnet_model.clear()
2968        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2969            self.model_parameters.call_parameters if param.type == 'magnetic']
2970        FittingUtilities.addHeadersToModel(self._magnet_model)
2971
2972    def shellNamesList(self):
2973        """
2974        Returns list of names of all multi-shell parameters
2975        E.g. for sld[n], radius[n], n=1..3 it will return
2976        [sld1, sld2, sld3, radius1, radius2, radius3]
2977        """
2978        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2979        top_index = self.kernel_module.multiplicity_info.number
2980        shell_names = []
2981        for i in range(1, top_index+1):
2982            for name in multi_names:
2983                shell_names.append(name+str(i))
2984        return shell_names
2985
2986    def addCheckedMagneticListToModel(self, param, model):
2987        """
2988        Wrapper for model update with a subset of magnetic parameters
2989        """
2990        if param.name[param.name.index(':')+1:] in self.shell_names:
2991            # check if two-digit shell number
2992            try:
2993                shell_index = int(param.name[-2:])
2994            except ValueError:
2995                shell_index = int(param.name[-1:])
2996
2997            if shell_index > self.current_shell_displayed:
2998                return
2999
3000        checked_list = [param.name,
3001                        str(param.default),
3002                        str(param.limits[0]),
3003                        str(param.limits[1]),
3004                        param.units]
3005
3006        self.magnet_params[param.name] = param.default
3007
3008        FittingUtilities.addCheckedListToModel(model, checked_list)
3009
3010    def enableStructureFactorControl(self, structure_factor):
3011        """
3012        Add structure factors to the list of parameters
3013        """
3014        if self.kernel_module.is_form_factor or structure_factor == 'None':
3015            self.enableStructureCombo()
3016        else:
3017            self.disableStructureCombo()
3018
3019    def addExtraShells(self):
3020        """
3021        Add a combobox for multiple shell display
3022        """
3023        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3024
3025        if param_length == 0:
3026            return
3027
3028        # cell 1: variable name
3029        item1 = QtGui.QStandardItem(param_name)
3030
3031        func = QtWidgets.QComboBox()
3032
3033        # cell 2: combobox
3034        item2 = QtGui.QStandardItem()
3035
3036        # cell 3: min value
3037        item3 = QtGui.QStandardItem()
3038
3039        # cell 4: max value
3040        item4 = QtGui.QStandardItem()
3041
3042        # cell 4: SLD button
3043        item5 = QtGui.QStandardItem()
3044        button = QtWidgets.QPushButton()
3045        button.setText("Show SLD Profile")
3046
3047        self._model_model.appendRow([item1, item2, item3, item4, item5])
3048
3049        # Beautify the row:  span columns 2-4
3050        shell_row = self._model_model.rowCount()
3051        shell_index = self._model_model.index(shell_row-1, 1)
3052        button_index = self._model_model.index(shell_row-1, 4)
3053
3054        self.lstParams.setIndexWidget(shell_index, func)
3055        self.lstParams.setIndexWidget(button_index, button)
3056        self._n_shells_row = shell_row - 1
3057
3058        # Get the default number of shells for the model
3059        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3060        shell_par = None
3061        for par in kernel_pars:
3062            if par.name == param_name:
3063                shell_par = par
3064                break
3065        if not shell_par:
3066            logger.error("Could not find %s in kernel parameters.", param_name)
3067        default_shell_count = shell_par.default
3068        shell_min = 0
3069        shell_max = 0
3070        try:
3071            shell_min = int(shell_par.limits[0])
3072            shell_max = int(shell_par.limits[1])
3073        except IndexError as ex:
3074            # no info about limits
3075            pass
3076        # don't update the kernel here - this data is display only
3077        self._model_model.blockSignals(True)
3078        item3.setText(str(shell_min))
3079        item4.setText(str(shell_max))
3080        self._model_model.blockSignals(False)
3081
3082        # Respond to index change
3083        func.currentTextChanged.connect(self.modifyShellsInList)
3084
3085        # Respond to button press
3086        button.clicked.connect(self.onShowSLDProfile)
3087
3088        # Available range of shells displayed in the combobox
3089        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3090
3091        # Add default number of shells to the model
3092        func.setCurrentText(str(default_shell_count))
3093
3094    def modifyShellsInList(self, text):
3095        """
3096        Add/remove additional multishell parameters
3097        """
3098        # Find row location of the combobox
3099        first_row = self._n_shells_row + 1
3100        remove_rows = self._num_shell_params
3101        try:
3102            index = int(text)
3103        except ValueError:
3104            # bad text on the control!
3105            index = 0
3106            logger.error("Multiplicity incorrect! Setting to 0")
3107        self.kernel_module.multiplicity = index
3108        if remove_rows > 1:
3109            self._model_model.removeRows(first_row, remove_rows)
3110
3111        new_rows = FittingUtilities.addShellsToModel(
3112                self.model_parameters,
3113                self._model_model,
3114                index,
3115                first_row,
3116                self.lstParams)
3117
3118        self._num_shell_params = len(new_rows)
3119        self.current_shell_displayed = index
3120
3121        # Param values for existing shells were reset to default; force all changes into kernel module
3122        for row in new_rows:
3123            par = row[0].text()
3124            val = GuiUtils.toDouble(row[1].text())
3125            self.kernel_module.setParam(par, val)
3126
3127        # Change 'n' in the parameter model; also causes recalculation
3128        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3129
3130        # Update relevant models
3131        self.setPolyModel()
3132        self.setMagneticModel()
3133
3134    def onShowSLDProfile(self):
3135        """
3136        Show a quick plot of SLD profile
3137        """
3138        # get profile data
3139        x, y = self.kernel_module.getProfile()
3140        y *= 1.0e6
3141        profile_data = Data1D(x=x, y=y)
3142        profile_data.name = "SLD"
3143        profile_data.scale = 'linear'
3144        profile_data.symbol = 'Line'
3145        profile_data.hide_error = True
3146        profile_data._xaxis = "R(\AA)"
3147        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3148
3149        plotter = PlotterWidget(self, quickplot=True)
3150        plotter.data = profile_data
3151        plotter.showLegend = True
3152        plotter.plot(hide_error=True, marker='-')
3153
3154        self.plot_widget = QtWidgets.QWidget()
3155        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3156        layout = QtWidgets.QVBoxLayout()
3157        layout.addWidget(plotter)
3158        self.plot_widget.setLayout(layout)
3159        self.plot_widget.show()
3160
3161    def setInteractiveElements(self, enabled=True):
3162        """
3163        Switch interactive GUI elements on/off
3164        """
3165        assert isinstance(enabled, bool)
3166
3167        self.lstParams.setEnabled(enabled)
3168        self.lstPoly.setEnabled(enabled)
3169        self.lstMagnetic.setEnabled(enabled)
3170
3171        self.cbCategory.setEnabled(enabled)
3172        self.cbModel.setEnabled(enabled)
3173        self.cbStructureFactor.setEnabled(enabled)
3174
3175    def enableInteractiveElements(self):
3176        """
3177        Set buttion caption on fitting/calculate finish
3178        Enable the param table(s)
3179        """
3180        # Notify the user that fitting is available
3181        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3182        self.cmdFit.setText("Fit")
3183        self.fit_started = False
3184        self.setInteractiveElements(True)
3185
3186    def disableInteractiveElements(self):
3187        """
3188        Set buttion caption on fitting/calculate start
3189        Disable the param table(s)
3190        """
3191        # Notify the user that fitting is being run
3192        # Allow for stopping the job
3193        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3194        self.cmdFit.setText('Stop fit')
3195        self.setInteractiveElements(False)
3196
3197    def readFitPage(self, fp):
3198        """
3199        Read in state from a fitpage object and update GUI
3200        """
3201        assert isinstance(fp, FitPage)
3202        # Main tab info
3203        self.logic.data.filename = fp.filename
3204        self.data_is_loaded = fp.data_is_loaded
3205        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3206        self.chkMagnetism.setCheckState(fp.is_magnetic)
3207        self.chk2DView.setCheckState(fp.is2D)
3208
3209        # Update the comboboxes
3210        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3211        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3212        if fp.current_factor:
3213            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3214
3215        self.chi2 = fp.chi2
3216
3217        # Options tab
3218        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3219        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3220        self.npts = fp.fit_options[fp.NPTS]
3221        self.log_points = fp.fit_options[fp.LOG_POINTS]
3222        self.weighting = fp.fit_options[fp.WEIGHTING]
3223
3224        # Models
3225        self._model_model = fp.model_model
3226        self._poly_model = fp.poly_model
3227        self._magnet_model = fp.magnetism_model
3228
3229        # Resolution tab
3230        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3231        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3232        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3233        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3234        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3235
3236        # TODO: add polidyspersity and magnetism
3237
3238    def saveToFitPage(self, fp):
3239        """
3240        Write current state to the given fitpage
3241        """
3242        assert isinstance(fp, FitPage)
3243
3244        # Main tab info
3245        fp.filename = self.logic.data.filename
3246        fp.data_is_loaded = self.data_is_loaded
3247        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3248        fp.is_magnetic = self.chkMagnetism.isChecked()
3249        fp.is2D = self.chk2DView.isChecked()
3250        fp.data = self.data
3251
3252        # Use current models - they contain all the required parameters
3253        fp.model_model = self._model_model
3254        fp.poly_model = self._poly_model
3255        fp.magnetism_model = self._magnet_model
3256
3257        if self.cbCategory.currentIndex() != 0:
3258            fp.current_category = str(self.cbCategory.currentText())
3259            fp.current_model = str(self.cbModel.currentText())
3260
3261        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3262            fp.current_factor = str(self.cbStructureFactor.currentText())
3263        else:
3264            fp.current_factor = ''
3265
3266        fp.chi2 = self.chi2
3267        fp.main_params_to_fit = self.main_params_to_fit
3268        fp.poly_params_to_fit = self.poly_params_to_fit
3269        fp.magnet_params_to_fit = self.magnet_params_to_fit
3270        fp.kernel_module = self.kernel_module
3271
3272        # Algorithm options
3273        # fp.algorithm = self.parent.fit_options.selected_id
3274
3275        # Options tab
3276        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3277        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3278        fp.fit_options[fp.NPTS] = self.npts
3279        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3280        fp.fit_options[fp.LOG_POINTS] = self.log_points
3281        fp.fit_options[fp.WEIGHTING] = self.weighting
3282
3283        # Resolution tab
3284        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3285        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3286        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3287        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3288        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3289
3290        # TODO: add polidyspersity and magnetism
3291
3292    def updateUndo(self):
3293        """
3294        Create a new state page and add it to the stack
3295        """
3296        if self.undo_supported:
3297            self.pushFitPage(self.currentState())
3298
3299    def currentState(self):
3300        """
3301        Return fit page with current state
3302        """
3303        new_page = FitPage()
3304        self.saveToFitPage(new_page)
3305
3306        return new_page
3307
3308    def pushFitPage(self, new_page):
3309        """
3310        Add a new fit page object with current state
3311        """
3312        self.page_stack.append(new_page)
3313
3314    def popFitPage(self):
3315        """
3316        Remove top fit page from stack
3317        """
3318        if self.page_stack:
3319            self.page_stack.pop()
3320
3321    def getReport(self):
3322        """
3323        Create and return HTML report with parameters and charts
3324        """
3325        index = None
3326        if self.all_data:
3327            index = self.all_data[self.data_index]
3328        else:
3329            index = self.theory_item
3330        report_logic = ReportPageLogic(self,
3331                                       kernel_module=self.kernel_module,
3332                                       data=self.data,
3333                                       index=index,
3334                                       model=self._model_model)
3335
3336        return report_logic.reportList()
3337
3338    def savePageState(self):
3339        """
3340        Create and serialize local PageState
3341        """
3342        from sas.sascalc.fit.pagestate import Reader
3343        model = self.kernel_module
3344
3345        # Old style PageState object
3346        state = PageState(model=model, data=self.data)
3347
3348        # Add parameter data to the state
3349        self.getCurrentFitState(state)
3350
3351        # Create the filewriter, aptly named 'Reader'
3352        state_reader = Reader(self.loadPageStateCallback)
3353        filepath = self.saveAsAnalysisFile()
3354        if filepath is None or filepath == "":
3355            return
3356        state_reader.write(filename=filepath, fitstate=state)
3357        pass
3358
3359    def saveAsAnalysisFile(self):
3360        """
3361        Show the save as... dialog and return the chosen filepath
3362        """
3363        default_name = "FitPage"+str(self.tab_id)+".fitv"
3364
3365        wildcard = "fitv files (*.fitv)"
3366        kwargs = {
3367            'caption'   : 'Save As',
3368            'directory' : default_name,
3369            'filter'    : wildcard,
3370            'parent'    : None,
3371        }
3372        # Query user for filename.
3373        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3374        filename = filename_tuple[0]
3375        return filename
3376
3377    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3378        """
3379        This is a callback method called from the CANSAS reader.
3380        We need the instance of this reader only for writing out a file,
3381        so there's nothing here.
3382        Until Load Analysis is implemented, that is.
3383        """
3384        pass
3385
3386    def loadPageState(self, pagestate=None):
3387        """
3388        Load the PageState object and update the current widget
3389        """
3390        pass
3391
3392    def getCurrentFitState(self, state=None):
3393        """
3394        Store current state for fit_page
3395        """
3396        # save model option
3397        #if self.model is not None:
3398        #    self.disp_list = self.getDispParamList()
3399        #    state.disp_list = copy.deepcopy(self.disp_list)
3400        #    #state.model = self.model.clone()
3401
3402        # Comboboxes
3403        state.categorycombobox = self.cbCategory.currentText()
3404        state.formfactorcombobox = self.cbModel.currentText()
3405        if self.cbStructureFactor.isEnabled():
3406            state.structurecombobox = self.cbStructureFactor.currentText()
3407        state.tcChi = self.chi2
3408
3409        state.enable2D = self.is2D
3410
3411        #state.weights = copy.deepcopy(self.weights)
3412        # save data
3413        state.data = copy.deepcopy(self.data)
3414
3415        # save plotting range
3416        state.qmin = self.q_range_min
3417        state.qmax = self.q_range_max
3418        state.npts = self.npts
3419
3420        #    self.state.enable_disp = self.enable_disp.GetValue()
3421        #    self.state.disable_disp = self.disable_disp.GetValue()
3422
3423        #    self.state.enable_smearer = \
3424        #                        copy.deepcopy(self.enable_smearer.GetValue())
3425        #    self.state.disable_smearer = \
3426        #                        copy.deepcopy(self.disable_smearer.GetValue())
3427
3428        #self.state.pinhole_smearer = \
3429        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3430        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3431        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3432        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3433        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3434        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3435
3436        p = self.model_parameters
3437        # save checkbutton state and txtcrtl values
3438        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3439        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3440
3441        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3442        #self._copy_parameters_state(self.parameters, self.state.parameters)
3443        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3444        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3445
3446    def onParameterCopy(self, format=None):
3447        """
3448        Copy current parameters into the clipboard
3449        """
3450        # run a loop over all parameters and pull out
3451        # first - regular params
3452        param_list = []
3453
3454        param_list.append(['model_name', str(self.cbModel.currentText())])
3455        def gatherParams(row):
3456            """
3457            Create list of main parameters based on _model_model
3458            """
3459            param_name = str(self._model_model.item(row, 0).text())
3460            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3461            param_value = str(self._model_model.item(row, 1).text())
3462            param_error = None
3463            param_min = None
3464            param_max = None
3465            column_offset = 0
3466            if self.has_error_column:
3467                param_error = str(self._model_model.item(row, 2).text())
3468                column_offset = 1
3469
3470            try:
3471                param_min = str(self._model_model.item(row, 2+column_offset).text())
3472                param_max = str(self._model_model.item(row, 3+column_offset).text())
3473            except:
3474                pass
3475
3476            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3477
3478        def gatherPolyParams(row):
3479            """
3480            Create list of polydisperse parameters based on _poly_model
3481            """
3482            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3483            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3484            param_value = str(self._poly_model.item(row, 1).text())
3485            param_error = None
3486            column_offset = 0
3487            if self.has_poly_error_column:
3488                param_error = str(self._poly_model.item(row, 2).text())
3489                column_offset = 1
3490            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3491            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3492            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3493            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3494            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3495            # width
3496            name = param_name+".width"
3497            param_list.append([name, param_checked, param_value, param_error,
3498                                param_npts, param_nsigs, param_min, param_max, param_fun])
3499
3500        def gatherMagnetParams(row):
3501            """
3502            Create list of magnetic parameters based on _magnet_model
3503            """
3504            param_name = str(self._magnet_model.item(row, 0).text())
3505            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3506            param_value = str(self._magnet_model.item(row, 1).text())
3507            param_error = None
3508            column_offset = 0
3509            if self.has_magnet_error_column:
3510                param_error = str(self._magnet_model.item(row, 2).text())
3511                column_offset = 1
3512            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3513            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3514            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3515
3516        self.iterateOverModel(gatherParams)
3517        if self.chkPolydispersity.isChecked():
3518            self.iterateOverPolyModel(gatherPolyParams)
3519        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3520            self.iterateOverMagnetModel(gatherMagnetParams)
3521
3522        if format=="":
3523            formatted_output = FittingUtilities.formatParameters(param_list)
3524        elif format == "Excel":
3525            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3526        elif format == "Latex":
3527            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3528        else:
3529            raise AttributeError("Bad format specifier.")
3530
3531        # Dump formatted_output to the clipboard
3532        cb = QtWidgets.QApplication.clipboard()
3533        cb.setText(formatted_output)
3534
3535    def onParameterPaste(self):
3536        """
3537        Use the clipboard to update fit state
3538        """
3539        # Check if the clipboard contains right stuff
3540        cb = QtWidgets.QApplication.clipboard()
3541        cb_text = cb.text()
3542
3543        context = {}
3544        # put the text into dictionary
3545        lines = cb_text.split(':')
3546        if lines[0] != 'sasview_parameter_values':
3547            return False
3548
3549        model = lines[1].split(',')
3550
3551        if model[0] != 'model_name':
3552            return False
3553
3554        context['model_name'] = [model[1]]
3555        for line in lines[2:-1]:
3556            if len(line) != 0:
3557                item = line.split(',')
3558                check = item[1]
3559                name = item[0]
3560                value = item[2]
3561                # Transfer the text to content[dictionary]
3562                context[name] = [check, value]
3563
3564                # limits
3565                try:
3566                    limit_lo = item[3]
3567                    context[name].append(limit_lo)
3568                    limit_hi = item[4]
3569                    context[name].append(limit_hi)
3570                except:
3571                    pass
3572
3573                # Polydisp
3574                if len(item) > 5:
3575                    value = item[5]
3576                    context[name].append(value)
3577                    try:
3578                        value = item[6]
3579                        context[name].append(value)
3580                        value = item[7]
3581                        context[name].append(value)
3582                    except IndexError:
3583                        pass
3584
3585        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3586            msg = QtWidgets.QMessageBox()
3587            msg.setIcon(QtWidgets.QMessageBox.Information)
3588            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3589                         Not all parameters saved may paste correctly.")
3590            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3591            result = msg.exec_()
3592            if result == QtWidgets.QMessageBox.Ok:
3593                pass
3594            else:
3595                return
3596
3597        self.updateFullModel(context)
3598        self.updateFullPolyModel(context)
3599
3600    def updateFullModel(self, param_dict):
3601        """
3602        Update the model with new parameters
3603        """
3604        assert isinstance(param_dict, dict)
3605        if not dict:
3606            return
3607
3608        def updateFittedValues(row):
3609            # Utility function for main model update
3610            # internal so can use closure for param_dict
3611            param_name = str(self._model_model.item(row, 0).text())
3612            if param_name not in list(param_dict.keys()):
3613                return
3614            # checkbox state
3615            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3616            self._model_model.item(row, 0).setCheckState(param_checked)
3617
3618            # modify the param value
3619            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3620            self._model_model.item(row, 1).setText(param_repr)
3621
3622            # Potentially the error column
3623            ioffset = 0
3624            if len(param_dict[param_name])>4 and self.has_error_column:
3625                # error values are not editable - no need to update
3626                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3627                #self._model_model.item(row, 2).setText(error_repr)
3628                ioffset = 1
3629            # min/max
3630            try:
3631                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3632                self._model_model.item(row, 2+ioffset).setText(param_repr)
3633                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3634                self._model_model.item(row, 3+ioffset).setText(param_repr)
3635            except:
3636                pass
3637
3638            self.setFocus()
3639
3640
3641
3642        # block signals temporarily, so we don't end up
3643        # updating charts with every single model change on the end of fitting
3644        self._model_model.blockSignals(True)
3645        self.iterateOverModel(updateFittedValues)
3646        self._model_model.blockSignals(False)
3647
3648
3649    def updateFullPolyModel(self, param_dict):
3650        """
3651        Update the polydispersity model with new parameters, create the errors column
3652        """
3653        assert isinstance(param_dict, dict)
3654        if not dict:
3655            return
3656
3657        def updateFittedValues(row):
3658            # Utility function for main model update
3659            # internal so can use closure for param_dict
3660            if row >= self._poly_model.rowCount():
3661                return
3662            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3663            if param_name not in list(param_dict.keys()):
3664                return
3665            # checkbox state
3666            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3667            self._poly_model.item(row,0).setCheckState(param_checked)
3668
3669            # modify the param value
3670            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3671            self._poly_model.item(row, 1).setText(param_repr)
3672
3673            # Potentially the error column
3674            ioffset = 0
3675            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3676                ioffset = 1
3677            # min
3678            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3679            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3680            # max
3681            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3682            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3683            # Npts
3684            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3685            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3686            # Nsigs
3687            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3688            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3689
3690            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3691            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3692            self.setFocus()
3693
3694        # block signals temporarily, so we don't end up
3695        # updating charts with every single model change on the end of fitting
3696        self._poly_model.blockSignals(True)
3697        self.iterateOverPolyModel(updateFittedValues)
3698        self._poly_model.blockSignals(False)
3699
3700
3701
Note: See TracBrowser for help on using the repository browser.