source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 48df831

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

Fix the merge

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