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

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

Added C&S tab serialization.

  • Property mode set to 100644
File size: 154.0 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 = self.kernel_module.name
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        # Convert to proper indices and set requested enablement
2451        self.setParameterSelection(status)
2452
2453        # update the list of parameters to fit
2454        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2455
2456    def checkedListFromModel(self, model):
2457        """
2458        Returns list of checked parameters for given model
2459        """
2460        def isChecked(row):
2461            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2462
2463        return [str(model.item(row_index, 0).text())
2464                for row_index in range(model.rowCount())
2465                if isChecked(row_index)]
2466
2467    def createNewIndex(self, fitted_data):
2468        """
2469        Create a model or theory index with passed Data1D/Data2D
2470        """
2471        if self.data_is_loaded:
2472            if not fitted_data.name:
2473                name = self.nameForFittedData(self.data.filename)
2474                fitted_data.title = name
2475                fitted_data.name = name
2476                fitted_data.filename = name
2477                fitted_data.symbol = "Line"
2478            self.updateModelIndex(fitted_data)
2479        else:
2480            if not fitted_data.name:
2481                name = self.nameForFittedData(self.kernel_module.id)
2482            else:
2483                name = fitted_data.name
2484            fitted_data.title = name
2485            fitted_data.filename = name
2486            fitted_data.symbol = "Line"
2487            self.createTheoryIndex(fitted_data)
2488            # Switch to the theory tab for user's glee
2489            self.communicate.changeDataExplorerTabSignal.emit(1)
2490
2491    def updateModelIndex(self, fitted_data):
2492        """
2493        Update a QStandardModelIndex containing model data
2494        """
2495        name = self.nameFromData(fitted_data)
2496        # Make this a line if no other defined
2497        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2498            fitted_data.symbol = 'Line'
2499        # Notify the GUI manager so it can update the main model in DataExplorer
2500        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2501
2502    def createTheoryIndex(self, fitted_data):
2503        """
2504        Create a QStandardModelIndex containing model data
2505        """
2506        name = self.nameFromData(fitted_data)
2507        # Notify the GUI manager so it can create the theory model in DataExplorer
2508        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2509        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2510
2511    def nameFromData(self, fitted_data):
2512        """
2513        Return name for the dataset. Terribly impure function.
2514        """
2515        if fitted_data.name is None:
2516            name = self.nameForFittedData(self.logic.data.filename)
2517            fitted_data.title = name
2518            fitted_data.name = name
2519            fitted_data.filename = name
2520        else:
2521            name = fitted_data.name
2522        return name
2523
2524    def methodCalculateForData(self):
2525        '''return the method for data calculation'''
2526        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2527
2528    def methodCompleteForData(self):
2529        '''return the method for result parsin on calc complete '''
2530        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2531
2532    def updateKernelModelWithExtraParams(self, model=None):
2533        """
2534        Updates kernel model 'model' with extra parameters from
2535        the polydisp and magnetism tab, if the tabs are enabled
2536        """
2537        if model is None: return
2538        if not hasattr(model, 'setParam'): return
2539
2540        # add polydisperse parameters if asked
2541        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2542            for key, value in self.poly_params.items():
2543                model.setParam(key, value)
2544        # add magnetic params if asked
2545        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
2546            for key, value in self.magnet_params.items():
2547                model.setParam(key, value)
2548
2549    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2550        """
2551        Wrapper for Calc1D/2D calls
2552        """
2553        if data is None:
2554            data = self.data
2555        if model is None:
2556            model = copy.deepcopy(self.kernel_module)
2557            self.updateKernelModelWithExtraParams(model)
2558
2559        if completefn is None:
2560            completefn = self.methodCompleteForData()
2561        smearer = self.smearing_widget.smearer()
2562        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2563
2564        # Disable buttons/table
2565        self.disableInteractiveElements()
2566        # Awful API to a backend method.
2567        calc_thread = self.methodCalculateForData()(data=data,
2568                                               model=model,
2569                                               page_id=0,
2570                                               qmin=self.q_range_min,
2571                                               qmax=self.q_range_max,
2572                                               smearer=smearer,
2573                                               state=None,
2574                                               weight=weight,
2575                                               fid=None,
2576                                               toggle_mode_on=False,
2577                                               completefn=completefn,
2578                                               update_chisqr=True,
2579                                               exception_handler=self.calcException,
2580                                               source=None)
2581        if use_threads:
2582            if LocalConfig.USING_TWISTED:
2583                # start the thread with twisted
2584                thread = threads.deferToThread(calc_thread.compute)
2585                thread.addCallback(completefn)
2586                thread.addErrback(self.calculateDataFailed)
2587            else:
2588                # Use the old python threads + Queue
2589                calc_thread.queue()
2590                calc_thread.ready(2.5)
2591        else:
2592            results = calc_thread.compute()
2593            completefn(results)
2594
2595    def calculateQGridForModel(self):
2596        """
2597        Prepare the fitting data object, based on current ModelModel
2598        """
2599        if self.kernel_module is None:
2600            return
2601        self.calculateQGridForModelExt()
2602
2603    def calculateDataFailed(self, reason):
2604        """
2605        Thread returned error
2606        """
2607        # Bring the GUI to normal state
2608        self.enableInteractiveElements()
2609        print("Calculate Data failed with ", reason)
2610
2611    def completed1D(self, return_data):
2612        self.Calc1DFinishedSignal.emit(return_data)
2613
2614    def completed2D(self, return_data):
2615        self.Calc2DFinishedSignal.emit(return_data)
2616
2617    def complete1D(self, return_data):
2618        """
2619        Plot the current 1D data
2620        """
2621        # Bring the GUI to normal state
2622        self.enableInteractiveElements()
2623        if return_data is None:
2624            return
2625        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2626
2627        # assure the current index is set properly for batch
2628        if len(self._logic) > 1:
2629            for i, logic in enumerate(self._logic):
2630                if logic.data.name in fitted_data.name:
2631                    self.data_index = i
2632
2633        residuals = self.calculateResiduals(fitted_data)
2634        self.model_data = fitted_data
2635        new_plots = [fitted_data]
2636        if residuals is not None:
2637            new_plots.append(residuals)
2638
2639        if self.data_is_loaded:
2640            # delete any plots associated with the data that were not updated
2641            # (e.g. to remove beta(Q), S_eff(Q))
2642            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2643            pass
2644        else:
2645            # delete theory items for the model, in order to get rid of any
2646            # redundant items, e.g. beta(Q), S_eff(Q)
2647            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2648
2649        # Create plots for parameters with enabled polydispersity
2650        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2651            data_id = fitted_data.id.split()
2652            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2653            data_name = fitted_data.name.split()
2654            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2655            self.createNewIndex(plot)
2656            new_plots.append(plot)
2657
2658        # Create plots for intermediate product data
2659        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2660        for plot in plots:
2661            plot.symbol = "Line"
2662            self.createNewIndex(plot)
2663            new_plots.append(plot)
2664
2665        for plot in new_plots:
2666            self.communicate.plotUpdateSignal.emit([plot])
2667
2668        # Update radius_effective if relevant
2669        self.updateEffectiveRadius(return_data)
2670
2671    def complete2D(self, return_data):
2672        """
2673        Plot the current 2D data
2674        """
2675        # Bring the GUI to normal state
2676        self.enableInteractiveElements()
2677
2678        if return_data is None:
2679            return
2680
2681        fitted_data = self.logic.new2DPlot(return_data)
2682        # assure the current index is set properly for batch
2683        if len(self._logic) > 1:
2684            for i, logic in enumerate(self._logic):
2685                if logic.data.name in fitted_data.name:
2686                    self.data_index = i
2687
2688        residuals = self.calculateResiduals(fitted_data)
2689        self.model_data = fitted_data
2690        new_plots = [fitted_data]
2691        if residuals is not None:
2692            new_plots.append(residuals)
2693
2694        # Update/generate plots
2695        for plot in new_plots:
2696            self.communicate.plotUpdateSignal.emit([plot])
2697
2698    def updateEffectiveRadius(self, return_data):
2699        """
2700        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2701        calculated value as returned by sasmodels (if the value was returned).
2702        """
2703        ER_mode_row = self.getRowFromName("radius_effective_mode")
2704        if ER_mode_row is None:
2705            return
2706        try:
2707            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2708        except ValueError:
2709            logging.error("radius_effective_mode was set to an invalid value.")
2710            return
2711        if ER_mode < 1:
2712            # does not need updating if it is not being computed
2713            return
2714
2715        ER_row = self.getRowFromName("radius_effective")
2716        if ER_row is None:
2717            return
2718
2719        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2720        ER_value = scalar_results.get("effective_radius") # note name of key
2721        if ER_value is None:
2722            return
2723        # ensure the model does not recompute when updating the value
2724        self._model_model.blockSignals(True)
2725        self._model_model.item(ER_row, 1).setText(str(ER_value))
2726        self._model_model.blockSignals(False)
2727        # ensure the view is updated immediately
2728        self._model_model.layoutChanged.emit()
2729
2730    def calculateResiduals(self, fitted_data):
2731        """
2732        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2733        """
2734        # Create a new index for holding data
2735        fitted_data.symbol = "Line"
2736
2737        # Modify fitted_data with weighting
2738        weighted_data = self.addWeightingToData(fitted_data)
2739
2740        self.createNewIndex(weighted_data)
2741
2742        # Calculate difference between return_data and logic.data
2743        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2744        # Update the control
2745        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2746        self.lblChi2Value.setText(chi2_repr)
2747
2748        # Plot residuals if actual data
2749        if not self.data_is_loaded:
2750            return
2751
2752        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2753        if residuals_plot is None:
2754            return
2755        residuals_plot.id = "Residual " + residuals_plot.id
2756        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2757        self.createNewIndex(residuals_plot)
2758        return residuals_plot
2759
2760    def onCategoriesChanged(self):
2761            """
2762            Reload the category/model comboboxes
2763            """
2764            # Store the current combo indices
2765            current_cat = self.cbCategory.currentText()
2766            current_model = self.cbModel.currentText()
2767
2768            # reread the category file and repopulate the combo
2769            self.cbCategory.blockSignals(True)
2770            self.cbCategory.clear()
2771            self.readCategoryInfo()
2772            self.initializeCategoryCombo()
2773
2774            # Scroll back to the original index in Categories
2775            new_index = self.cbCategory.findText(current_cat)
2776            if new_index != -1:
2777                self.cbCategory.setCurrentIndex(new_index)
2778            self.cbCategory.blockSignals(False)
2779            # ...and in the Models
2780            self.cbModel.blockSignals(True)
2781            new_index = self.cbModel.findText(current_model)
2782            if new_index != -1:
2783                self.cbModel.setCurrentIndex(new_index)
2784            self.cbModel.blockSignals(False)
2785
2786            return
2787
2788    def calcException(self, etype, value, tb):
2789        """
2790        Thread threw an exception.
2791        """
2792        # Bring the GUI to normal state
2793        self.enableInteractiveElements()
2794        # TODO: remimplement thread cancellation
2795        logger.error("".join(traceback.format_exception(etype, value, tb)))
2796
2797    def setTableProperties(self, table):
2798        """
2799        Setting table properties
2800        """
2801        # Table properties
2802        table.verticalHeader().setVisible(False)
2803        table.setAlternatingRowColors(True)
2804        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2805        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2806        table.resizeColumnsToContents()
2807
2808        # Header
2809        header = table.horizontalHeader()
2810        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2811        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2812
2813        # Qt5: the following 2 lines crash - figure out why!
2814        # Resize column 0 and 7 to content
2815        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2816        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2817
2818    def setPolyModel(self):
2819        """
2820        Set polydispersity values
2821        """
2822        if not self.model_parameters:
2823            return
2824        self._poly_model.clear()
2825
2826        parameters = self.model_parameters.form_volume_parameters
2827        if self.is2D:
2828            parameters += self.model_parameters.orientation_parameters
2829
2830        [self.setPolyModelParameters(i, param) for i, param in \
2831            enumerate(parameters) if param.polydisperse]
2832
2833        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2834
2835    def setPolyModelParameters(self, i, param):
2836        """
2837        Standard of multishell poly parameter driver
2838        """
2839        param_name = param.name
2840        # see it the parameter is multishell
2841        if '[' in param.name:
2842            # Skip empty shells
2843            if self.current_shell_displayed == 0:
2844                return
2845            else:
2846                # Create as many entries as current shells
2847                for ishell in range(1, self.current_shell_displayed+1):
2848                    # Remove [n] and add the shell numeral
2849                    name = param_name[0:param_name.index('[')] + str(ishell)
2850                    self.addNameToPolyModel(i, name)
2851        else:
2852            # Just create a simple param entry
2853            self.addNameToPolyModel(i, param_name)
2854
2855    def addNameToPolyModel(self, i, param_name):
2856        """
2857        Creates a checked row in the poly model with param_name
2858        """
2859        # Polydisp. values from the sasmodel
2860        width = self.kernel_module.getParam(param_name + '.width')
2861        npts = self.kernel_module.getParam(param_name + '.npts')
2862        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2863        _, min, max = self.kernel_module.details[param_name]
2864
2865        # Update local param dict
2866        self.poly_params[param_name + '.width'] = width
2867        self.poly_params[param_name + '.npts'] = npts
2868        self.poly_params[param_name + '.nsigmas'] = nsigs
2869
2870        # Construct a row with polydisp. related variable.
2871        # This will get added to the polydisp. model
2872        # Note: last argument needs extra space padding for decent display of the control
2873        checked_list = ["Distribution of " + param_name, str(width),
2874                        str(min), str(max),
2875                        str(npts), str(nsigs), "gaussian      ",'']
2876        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2877
2878        # All possible polydisp. functions as strings in combobox
2879        func = QtWidgets.QComboBox()
2880        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2881        # Set the default index
2882        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2883        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2884        self.lstPoly.setIndexWidget(ind, func)
2885        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2886
2887    def onPolyFilenameChange(self, row_index):
2888        """
2889        Respond to filename_updated signal from the delegate
2890        """
2891        # For the given row, invoke the "array" combo handler
2892        array_caption = 'array'
2893
2894        # Get the combo box reference
2895        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2896        widget = self.lstPoly.indexWidget(ind)
2897
2898        # Update the combo box so it displays "array"
2899        widget.blockSignals(True)
2900        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2901        widget.blockSignals(False)
2902
2903        # Invoke the file reader
2904        self.onPolyComboIndexChange(array_caption, row_index)
2905
2906    def onPolyComboIndexChange(self, combo_string, row_index):
2907        """
2908        Modify polydisp. defaults on function choice
2909        """
2910        # Get npts/nsigs for current selection
2911        param = self.model_parameters.form_volume_parameters[row_index]
2912        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2913        combo_box = self.lstPoly.indexWidget(file_index)
2914
2915        def updateFunctionCaption(row):
2916            # Utility function for update of polydispersity function name in the main model
2917            if not self.isCheckable(row):
2918                return
2919            self._model_model.blockSignals(True)
2920            param_name = str(self._model_model.item(row, 0).text())
2921            self._model_model.blockSignals(False)
2922            if param_name !=  param.name:
2923                return
2924            # Modify the param value
2925            self._model_model.blockSignals(True)
2926            if self.has_error_column:
2927                # err column changes the indexing
2928                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2929            else:
2930                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2931            self._model_model.blockSignals(False)
2932
2933        if combo_string == 'array':
2934            try:
2935                self.loadPolydispArray(row_index)
2936                # Update main model for display
2937                self.iterateOverModel(updateFunctionCaption)
2938                # disable the row
2939                lo = self.lstPoly.itemDelegate().poly_pd
2940                hi = self.lstPoly.itemDelegate().poly_function
2941                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2942                return
2943            except IOError:
2944                combo_box.setCurrentIndex(self.orig_poly_index)
2945                # Pass for cancel/bad read
2946                pass
2947
2948        # Enable the row in case it was disabled by Array
2949        self._poly_model.blockSignals(True)
2950        max_range = self.lstPoly.itemDelegate().poly_filename
2951        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2952        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2953        self._poly_model.setData(file_index, "")
2954        self._poly_model.blockSignals(False)
2955
2956        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2957        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2958
2959        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2960        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2961
2962        self._poly_model.setData(npts_index, npts)
2963        self._poly_model.setData(nsigs_index, nsigs)
2964
2965        self.iterateOverModel(updateFunctionCaption)
2966        self.orig_poly_index = combo_box.currentIndex()
2967
2968    def loadPolydispArray(self, row_index):
2969        """
2970        Show the load file dialog and loads requested data into state
2971        """
2972        datafile = QtWidgets.QFileDialog.getOpenFileName(
2973            self, "Choose a weight file", "", "All files (*.*)", None,
2974            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2975
2976        if not datafile:
2977            logger.info("No weight data chosen.")
2978            raise IOError
2979
2980        values = []
2981        weights = []
2982        def appendData(data_tuple):
2983            """
2984            Fish out floats from a tuple of strings
2985            """
2986            try:
2987                values.append(float(data_tuple[0]))
2988                weights.append(float(data_tuple[1]))
2989            except (ValueError, IndexError):
2990                # just pass through if line with bad data
2991                return
2992
2993        with open(datafile, 'r') as column_file:
2994            column_data = [line.rstrip().split() for line in column_file.readlines()]
2995            [appendData(line) for line in column_data]
2996
2997        # If everything went well - update the sasmodel values
2998        self.disp_model = POLYDISPERSITY_MODELS['array']()
2999        self.disp_model.set_weights(np.array(values), np.array(weights))
3000        # + update the cell with filename
3001        fname = os.path.basename(str(datafile))
3002        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
3003        self._poly_model.setData(fname_index, fname)
3004
3005    def onColumnWidthUpdate(self, index, old_size, new_size):
3006        """
3007        Simple state update of the current column widths in the  param list
3008        """
3009        self.lstParamHeaderSizes[index] = new_size
3010
3011    def setMagneticModel(self):
3012        """
3013        Set magnetism values on model
3014        """
3015        if not self.model_parameters:
3016            return
3017        self._magnet_model.clear()
3018        # default initial value
3019        m0 = 0.5
3020        for param in self.model_parameters.call_parameters:
3021            if param.type != 'magnetic': continue
3022            if "M0" in param.name:
3023                m0 += 0.5
3024                value = m0
3025            else:
3026                value = param.default
3027            self.addCheckedMagneticListToModel(param, value)
3028
3029        FittingUtilities.addHeadersToModel(self._magnet_model)
3030
3031    def shellNamesList(self):
3032        """
3033        Returns list of names of all multi-shell parameters
3034        E.g. for sld[n], radius[n], n=1..3 it will return
3035        [sld1, sld2, sld3, radius1, radius2, radius3]
3036        """
3037        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3038        top_index = self.kernel_module.multiplicity_info.number
3039        shell_names = []
3040        for i in range(1, top_index+1):
3041            for name in multi_names:
3042                shell_names.append(name+str(i))
3043        return shell_names
3044
3045    def addCheckedMagneticListToModel(self, param, value):
3046        """
3047        Wrapper for model update with a subset of magnetic parameters
3048        """
3049        try:
3050            basename, _ = param.name.rsplit('_', 1)
3051        except ValueError:
3052            basename = param.name
3053        if basename in self.shell_names:
3054            try:
3055                shell_index = int(basename[-2:])
3056            except ValueError:
3057                shell_index = int(basename[-1:])
3058
3059            if shell_index > self.current_shell_displayed:
3060                return
3061
3062        checked_list = [param.name,
3063                        str(value),
3064                        str(param.limits[0]),
3065                        str(param.limits[1]),
3066                        param.units]
3067
3068        self.magnet_params[param.name] = value
3069
3070        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
3071
3072    def enableStructureFactorControl(self, structure_factor):
3073        """
3074        Add structure factors to the list of parameters
3075        """
3076        if self.kernel_module.is_form_factor or structure_factor == 'None':
3077            self.enableStructureCombo()
3078        else:
3079            self.disableStructureCombo()
3080
3081    def addExtraShells(self):
3082        """
3083        Add a combobox for multiple shell display
3084        """
3085        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3086
3087        if param_length == 0:
3088            return
3089
3090        # cell 1: variable name
3091        item1 = QtGui.QStandardItem(param_name)
3092
3093        func = QtWidgets.QComboBox()
3094
3095        # cell 2: combobox
3096        item2 = QtGui.QStandardItem()
3097
3098        # cell 3: min value
3099        item3 = QtGui.QStandardItem()
3100
3101        # cell 4: max value
3102        item4 = QtGui.QStandardItem()
3103
3104        # cell 4: SLD button
3105        item5 = QtGui.QStandardItem()
3106        button = QtWidgets.QPushButton()
3107        button.setText("Show SLD Profile")
3108
3109        self._model_model.appendRow([item1, item2, item3, item4, item5])
3110
3111        # Beautify the row:  span columns 2-4
3112        shell_row = self._model_model.rowCount()
3113        shell_index = self._model_model.index(shell_row-1, 1)
3114        button_index = self._model_model.index(shell_row-1, 4)
3115
3116        self.lstParams.setIndexWidget(shell_index, func)
3117        self.lstParams.setIndexWidget(button_index, button)
3118        self._n_shells_row = shell_row - 1
3119
3120        # Get the default number of shells for the model
3121        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3122        shell_par = None
3123        for par in kernel_pars:
3124            parname = par.name
3125            if '[' in parname:
3126                 parname = parname[:parname.index('[')]
3127            if parname == param_name:
3128                shell_par = par
3129                break
3130        if shell_par is None:
3131            logger.error("Could not find %s in kernel parameters.", param_name)
3132            return
3133        default_shell_count = shell_par.default
3134        shell_min = 0
3135        shell_max = 0
3136        try:
3137            shell_min = int(shell_par.limits[0])
3138            shell_max = int(shell_par.limits[1])
3139        except IndexError as ex:
3140            # no info about limits
3141            pass
3142        except Exception as ex:
3143            logging.error("Badly defined multiplicity: "+ str(ex))
3144            return
3145        # don't update the kernel here - this data is display only
3146        self._model_model.blockSignals(True)
3147        item3.setText(str(shell_min))
3148        item4.setText(str(shell_max))
3149        self._model_model.blockSignals(False)
3150
3151        ## Respond to index change
3152        #func.currentTextChanged.connect(self.modifyShellsInList)
3153
3154        # Respond to button press
3155        button.clicked.connect(self.onShowSLDProfile)
3156
3157        # Available range of shells displayed in the combobox
3158        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3159
3160        # Respond to index change
3161        func.currentTextChanged.connect(self.modifyShellsInList)
3162
3163        # Add default number of shells to the model
3164        func.setCurrentText(str(default_shell_count))
3165        self.modifyShellsInList(str(default_shell_count))
3166
3167    def modifyShellsInList(self, text):
3168        """
3169        Add/remove additional multishell parameters
3170        """
3171        # Find row location of the combobox
3172        first_row = self._n_shells_row + 1
3173        remove_rows = self._num_shell_params
3174        try:
3175            index = int(text)
3176        except ValueError:
3177            # bad text on the control!
3178            index = 0
3179            logger.error("Multiplicity incorrect! Setting to 0")
3180        self.kernel_module.multiplicity = index
3181        if remove_rows > 1:
3182            self._model_model.removeRows(first_row, remove_rows)
3183
3184        new_rows = FittingUtilities.addShellsToModel(
3185                self.model_parameters,
3186                self._model_model,
3187                index,
3188                first_row,
3189                self.lstParams)
3190
3191        self._num_shell_params = len(new_rows)
3192        self.current_shell_displayed = index
3193
3194        # Param values for existing shells were reset to default; force all changes into kernel module
3195        for row in new_rows:
3196            par = row[0].text()
3197            val = GuiUtils.toDouble(row[1].text())
3198            self.kernel_module.setParam(par, val)
3199
3200        # Change 'n' in the parameter model; also causes recalculation
3201        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3202
3203        # Update relevant models
3204        self.setPolyModel()
3205        if self.canHaveMagnetism():
3206            self.setMagneticModel()
3207
3208    def onShowSLDProfile(self):
3209        """
3210        Show a quick plot of SLD profile
3211        """
3212        # get profile data
3213        try:
3214            x, y = self.kernel_module.getProfile()
3215        except TypeError:
3216            msg = "SLD profile calculation failed."
3217            logging.error(msg)
3218            return
3219
3220        y *= 1.0e6
3221        profile_data = Data1D(x=x, y=y)
3222        profile_data.name = "SLD"
3223        profile_data.scale = 'linear'
3224        profile_data.symbol = 'Line'
3225        profile_data.hide_error = True
3226        profile_data._xaxis = "R(\AA)"
3227        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3228
3229        plotter = PlotterWidget(self, quickplot=True)
3230        plotter.data = profile_data
3231        plotter.showLegend = True
3232        plotter.plot(hide_error=True, marker='-')
3233
3234        self.plot_widget = QtWidgets.QWidget()
3235        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3236        layout = QtWidgets.QVBoxLayout()
3237        layout.addWidget(plotter)
3238        self.plot_widget.setLayout(layout)
3239        self.plot_widget.show()
3240
3241    def setInteractiveElements(self, enabled=True):
3242        """
3243        Switch interactive GUI elements on/off
3244        """
3245        assert isinstance(enabled, bool)
3246
3247        self.lstParams.setEnabled(enabled)
3248        self.lstPoly.setEnabled(enabled)
3249        self.lstMagnetic.setEnabled(enabled)
3250
3251        self.cbCategory.setEnabled(enabled)
3252
3253        if enabled:
3254            # worry about original enablement of model and SF
3255            self.cbModel.setEnabled(self.enabled_cbmodel)
3256            self.cbStructureFactor.setEnabled(self.enabled_sfmodel)
3257        else:
3258            self.cbModel.setEnabled(enabled)
3259            self.cbStructureFactor.setEnabled(enabled)
3260
3261        self.cmdPlot.setEnabled(enabled)
3262
3263    def enableInteractiveElements(self):
3264        """
3265        Set buttion caption on fitting/calculate finish
3266        Enable the param table(s)
3267        """
3268        # Notify the user that fitting is available
3269        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3270        self.cmdFit.setText("Fit")
3271        self.fit_started = False
3272        self.setInteractiveElements(True)
3273
3274    def disableInteractiveElements(self):
3275        """
3276        Set buttion caption on fitting/calculate start
3277        Disable the param table(s)
3278        """
3279        # Notify the user that fitting is being run
3280        # Allow for stopping the job
3281        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3282        self.cmdFit.setText('Stop fit')
3283        self.setInteractiveElements(False)
3284
3285    def readFitPage(self, fp):
3286        """
3287        Read in state from a fitpage object and update GUI
3288        """
3289        assert isinstance(fp, FitPage)
3290        # Main tab info
3291        self.logic.data.filename = fp.filename
3292        self.data_is_loaded = fp.data_is_loaded
3293        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3294        self.chkMagnetism.setCheckState(fp.is_magnetic)
3295        self.chk2DView.setCheckState(fp.is2D)
3296
3297        # Update the comboboxes
3298        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3299        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3300        if fp.current_factor:
3301            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3302
3303        self.chi2 = fp.chi2
3304
3305        # Options tab
3306        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3307        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3308        self.npts = fp.fit_options[fp.NPTS]
3309        self.log_points = fp.fit_options[fp.LOG_POINTS]
3310        self.weighting = fp.fit_options[fp.WEIGHTING]
3311
3312        # Models
3313        self._model_model = fp.model_model
3314        self._poly_model = fp.poly_model
3315        self._magnet_model = fp.magnetism_model
3316
3317        # Resolution tab
3318        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3319        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3320        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3321        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3322        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3323
3324        # TODO: add polidyspersity and magnetism
3325
3326    def saveToFitPage(self, fp):
3327        """
3328        Write current state to the given fitpage
3329        """
3330        assert isinstance(fp, FitPage)
3331
3332        # Main tab info
3333        fp.filename = self.logic.data.filename
3334        fp.data_is_loaded = self.data_is_loaded
3335        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3336        fp.is_magnetic = self.chkMagnetism.isChecked()
3337        fp.is2D = self.chk2DView.isChecked()
3338        fp.data = self.data
3339
3340        # Use current models - they contain all the required parameters
3341        fp.model_model = self._model_model
3342        fp.poly_model = self._poly_model
3343        fp.magnetism_model = self._magnet_model
3344
3345        if self.cbCategory.currentIndex() != 0:
3346            fp.current_category = str(self.cbCategory.currentText())
3347            fp.current_model = str(self.cbModel.currentText())
3348
3349        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3350            fp.current_factor = str(self.cbStructureFactor.currentText())
3351        else:
3352            fp.current_factor = ''
3353
3354        fp.chi2 = self.chi2
3355        fp.main_params_to_fit = self.main_params_to_fit
3356        fp.poly_params_to_fit = self.poly_params_to_fit
3357        fp.magnet_params_to_fit = self.magnet_params_to_fit
3358        fp.kernel_module = self.kernel_module
3359
3360        # Algorithm options
3361        # fp.algorithm = self.parent.fit_options.selected_id
3362
3363        # Options tab
3364        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3365        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3366        fp.fit_options[fp.NPTS] = self.npts
3367        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3368        fp.fit_options[fp.LOG_POINTS] = self.log_points
3369        fp.fit_options[fp.WEIGHTING] = self.weighting
3370
3371        # Resolution tab
3372        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3373        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3374        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3375        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3376        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3377
3378        # TODO: add polidyspersity and magnetism
3379
3380    def updateUndo(self):
3381        """
3382        Create a new state page and add it to the stack
3383        """
3384        if self.undo_supported:
3385            self.pushFitPage(self.currentState())
3386
3387    def currentState(self):
3388        """
3389        Return fit page with current state
3390        """
3391        new_page = FitPage()
3392        self.saveToFitPage(new_page)
3393
3394        return new_page
3395
3396    def pushFitPage(self, new_page):
3397        """
3398        Add a new fit page object with current state
3399        """
3400        self.page_stack.append(new_page)
3401
3402    def popFitPage(self):
3403        """
3404        Remove top fit page from stack
3405        """
3406        if self.page_stack:
3407            self.page_stack.pop()
3408
3409    def getReport(self):
3410        """
3411        Create and return HTML report with parameters and charts
3412        """
3413        index = None
3414        if self.all_data:
3415            index = self.all_data[self.data_index]
3416        else:
3417            index = self.theory_item
3418        params = FittingUtilities.getStandardParam(self._model_model)
3419        report_logic = ReportPageLogic(self,
3420                                       kernel_module=self.kernel_module,
3421                                       data=self.data,
3422                                       index=index,
3423                                       params=params)
3424
3425        return report_logic.reportList()
3426
3427    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3428        """
3429        This is a callback method called from the CANSAS reader.
3430        We need the instance of this reader only for writing out a file,
3431        so there's nothing here.
3432        Until Load Analysis is implemented, that is.
3433        """
3434        pass
3435
3436    def loadPageState(self, pagestate=None):
3437        """
3438        Load the PageState object and update the current widget
3439        """
3440        filepath = self.loadAnalysisFile()
3441        if filepath is None or filepath == "":
3442            return
3443
3444        with open(filepath, 'r') as statefile:
3445            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3446            lines = statefile.readlines()
3447
3448        # convert into list of lists
3449        pass
3450
3451    def loadAnalysisFile(self):
3452        """
3453        Called when the "Open Project" menu item chosen.
3454        """
3455        default_name = "FitPage"+str(self.tab_id)+".fitv"
3456        wildcard = "fitv files (*.fitv)"
3457        kwargs = {
3458            'caption'   : 'Open Analysis',
3459            'directory' : default_name,
3460            'filter'    : wildcard,
3461            'parent'    : self,
3462        }
3463        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3464        return filename
3465
3466    def onCopyToClipboard(self, format=None):
3467        """
3468        Copy current fitting parameters into the clipboard
3469        using requested formatting:
3470        plain, excel, latex
3471        """
3472        param_list = self.getFitParameters()
3473        if format=="":
3474            param_list = self.getFitPage()
3475            param_list += self.getFitModel()
3476            formatted_output = FittingUtilities.formatParameters(param_list)
3477        elif format == "Excel":
3478            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3479        elif format == "Latex":
3480            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3481        else:
3482            raise AttributeError("Bad parameter output format specifier.")
3483
3484        # Dump formatted_output to the clipboard
3485        cb = QtWidgets.QApplication.clipboard()
3486        cb.setText(formatted_output)
3487
3488    def getFitModel(self):
3489        """
3490        serializes combobox state
3491        """
3492        param_list = []
3493        model = str(self.cbModel.currentText())
3494        category = str(self.cbCategory.currentText())
3495        structure = str(self.cbStructureFactor.currentText())
3496        param_list.append(['fitpage_category', category])
3497        param_list.append(['fitpage_model', model])
3498        param_list.append(['fitpage_structure', structure])
3499
3500        return param_list
3501
3502    def getFitPage(self):
3503        """
3504        serializes full state of this fit page
3505        """
3506        # run a loop over all parameters and pull out
3507        # first - regular params
3508        param_list = self.getFitParameters()
3509
3510        param_list.append(['is_data', str(self.data_is_loaded)])
3511        data_ids = []
3512        filenames = []
3513        if self.is_batch_fitting:
3514            for item in self.all_data:
3515                # need item->data->data_id
3516                data = GuiUtils.dataFromItem(item)
3517                data_ids.append(data.id)
3518                filenames.append(data.filename)
3519        else:
3520            if self.data_is_loaded:
3521                data_ids = [str(self.logic.data.id)]
3522                filenames = [str(self.logic.data.filename)]
3523        param_list.append(['is_batch_fitting', str(self.is_batch_fitting)])
3524        param_list.append(['data_name', filenames])
3525        param_list.append(['data_id', data_ids])
3526        param_list.append(['tab_name', self.modelName()])
3527        # option tab
3528        param_list.append(['q_range_min', str(self.q_range_min)])
3529        param_list.append(['q_range_max', str(self.q_range_max)])
3530        param_list.append(['q_weighting', str(self.weighting)])
3531        param_list.append(['weighting', str(self.options_widget.weighting)])
3532
3533        # resolution
3534        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3535        index = self.smearing_widget.cbSmearing.currentIndex()
3536        param_list.append(['smearing', str(index)])
3537        param_list.append(['smearing_min', str(smearing_min)])
3538        param_list.append(['smearing_max', str(smearing_max)])
3539
3540        # checkboxes, if required
3541        has_polydisp = self.chkPolydispersity.isChecked()
3542        has_magnetism = self.chkMagnetism.isChecked()
3543        has_chain = self.chkChainFit.isChecked()
3544        has_2D = self.chk2DView.isChecked()
3545        param_list.append(['polydisperse_params', str(has_polydisp)])
3546        param_list.append(['magnetic_params', str(has_magnetism)])
3547        param_list.append(['chainfit_params', str(has_chain)])
3548        param_list.append(['2D_params', str(has_2D)])
3549
3550        return param_list
3551
3552    def getFitParameters(self):
3553        """
3554        serializes current parameters
3555        """
3556        param_list = []
3557        if self.kernel_module is None:
3558            return param_list
3559
3560        param_list.append(['model_name', str(self.cbModel.currentText())])
3561
3562        def gatherParams(row):
3563            """
3564            Create list of main parameters based on _model_model
3565            """
3566            param_name = str(self._model_model.item(row, 0).text())
3567
3568            # Assure this is a parameter - must contain a checkbox
3569            if not self._model_model.item(row, 0).isCheckable():
3570                # maybe it is a combobox item (multiplicity)
3571                try:
3572                    index = self._model_model.index(row, 1)
3573                    widget = self.lstParams.indexWidget(index)
3574                    if widget is None:
3575                        return
3576                    if isinstance(widget, QtWidgets.QComboBox):
3577                        # find the index of the combobox
3578                        current_index = widget.currentIndex()
3579                        param_list.append([param_name, 'None', str(current_index)])
3580                except Exception as ex:
3581                    pass
3582                return
3583
3584            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3585            # Value of the parameter. In some cases this is the text of the combobox choice.
3586            param_value = str(self._model_model.item(row, 1).text())
3587            param_error = None
3588            param_min = None
3589            param_max = None
3590            column_offset = 0
3591            if self.has_error_column:
3592                column_offset = 1
3593                param_error = str(self._model_model.item(row, 1+column_offset).text())
3594            try:
3595                param_min = str(self._model_model.item(row, 2+column_offset).text())
3596                param_max = str(self._model_model.item(row, 3+column_offset).text())
3597            except:
3598                pass
3599            # Do we have any constraints on this parameter?
3600            constraint = self.getConstraintForRow(row)
3601            cons = ()
3602            if constraint is not None:
3603                value = constraint.value
3604                func = constraint.func
3605                cons = (value, func)
3606
3607            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max, cons])
3608
3609        def gatherPolyParams(row):
3610            """
3611            Create list of polydisperse parameters based on _poly_model
3612            """
3613            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3614            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3615            param_value = str(self._poly_model.item(row, 1).text())
3616            param_error = None
3617            column_offset = 0
3618            if self.has_poly_error_column:
3619                column_offset = 1
3620                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3621            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3622            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3623            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3624            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3625            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3626            index = self._poly_model.index(row, 6+column_offset)
3627            widget = self.lstPoly.indexWidget(index)
3628            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3629                param_fun = widget.currentText()
3630            # width
3631            name = param_name+".width"
3632            param_list.append([name, param_checked, param_value, param_error,
3633                               param_min, param_max, param_npts, param_nsigs, param_fun])
3634
3635        def gatherMagnetParams(row):
3636            """
3637            Create list of magnetic parameters based on _magnet_model
3638            """
3639            param_name = str(self._magnet_model.item(row, 0).text())
3640            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3641            param_value = str(self._magnet_model.item(row, 1).text())
3642            param_error = None
3643            column_offset = 0
3644            if self.has_magnet_error_column:
3645                column_offset = 1
3646                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3647            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3648            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3649            param_list.append([param_name, param_checked, param_value,
3650                               param_error, param_min, param_max])
3651
3652        self.iterateOverModel(gatherParams)
3653        if self.chkPolydispersity.isChecked():
3654            self.iterateOverPolyModel(gatherPolyParams)
3655        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3656            self.iterateOverMagnetModel(gatherMagnetParams)
3657
3658        if self.kernel_module.is_multiplicity_model:
3659            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3660
3661        return param_list
3662
3663    def onParameterPaste(self):
3664        """
3665        Use the clipboard to update fit state
3666        """
3667        # Check if the clipboard contains right stuff
3668        cb = QtWidgets.QApplication.clipboard()
3669        cb_text = cb.text()
3670
3671        lines = cb_text.split(':')
3672        if lines[0] != 'sasview_parameter_values':
3673            return False
3674
3675        # put the text into dictionary
3676        line_dict = {}
3677        for line in lines[1:]:
3678            content = line.split(',')
3679            if len(content) > 1:
3680                line_dict[content[0]] = content[1:]
3681
3682        self.updatePageWithParameters(line_dict)
3683
3684    def createPageForParameters(self, line_dict):
3685        """
3686        Sets up page with requested model/str factor
3687        and fills it up with sent parameters
3688        """
3689        if 'fitpage_category' in line_dict:
3690            self.cbCategory.setCurrentIndex(self.cbCategory.findText(line_dict['fitpage_category'][0]))
3691        if 'fitpage_model' in line_dict:
3692            self.cbModel.setCurrentIndex(self.cbModel.findText(line_dict['fitpage_model'][0]))
3693        if 'fitpage_structure' in line_dict:
3694            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(line_dict['fitpage_structure'][0]))
3695
3696        # Now that the page is ready for parameters, fill it up
3697        self.updatePageWithParameters(line_dict)
3698
3699    def updatePageWithParameters(self, line_dict):
3700        """
3701        Update FitPage with parameters in line_dict
3702        """
3703        if 'model_name' not in line_dict.keys():
3704            return
3705        model = line_dict['model_name'][0]
3706        context = {}
3707
3708        if 'multiplicity' in line_dict.keys():
3709            multip = int(line_dict['multiplicity'][0], 0)
3710            # reset the model with multiplicity, so further updates are saved
3711            if self.kernel_module.is_multiplicity_model:
3712                self.kernel_module.multiplicity=multip
3713                self.updateMultiplicityCombo(multip)
3714
3715        if 'tab_name' in line_dict.keys():
3716            self.kernel_module.name = line_dict['tab_name'][0]
3717        if 'polydisperse_params' in line_dict.keys():
3718            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3719        if 'magnetic_params' in line_dict.keys():
3720            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3721        if 'chainfit_params' in line_dict.keys():
3722            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3723        if '2D_params' in line_dict.keys():
3724            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3725
3726        # Create the context dictionary for parameters
3727        context['model_name'] = model
3728        for key, value in line_dict.items():
3729            if len(value) > 2:
3730                context[key] = value
3731
3732        if str(self.cbModel.currentText()) != str(context['model_name']):
3733            msg = QtWidgets.QMessageBox()
3734            msg.setIcon(QtWidgets.QMessageBox.Information)
3735            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3736                         Not all parameters saved may paste correctly.")
3737            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3738            result = msg.exec_()
3739            if result == QtWidgets.QMessageBox.Ok:
3740                pass
3741            else:
3742                return
3743
3744        if 'smearing' in line_dict.keys():
3745            try:
3746                index = int(line_dict['smearing'][0])
3747                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3748            except ValueError:
3749                pass
3750        if 'smearing_min' in line_dict.keys():
3751            try:
3752                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3753            except ValueError:
3754                pass
3755        if 'smearing_max' in line_dict.keys():
3756            try:
3757                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3758            except ValueError:
3759                pass
3760
3761        if 'q_range_max' in line_dict.keys():
3762            try:
3763                self.q_range_min = float(line_dict['q_range_min'][0])
3764                self.q_range_max = float(line_dict['q_range_max'][0])
3765            except ValueError:
3766                pass
3767        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3768        try:
3769            button_id = int(line_dict['weighting'][0])
3770            for button in self.options_widget.weightingGroup.buttons():
3771                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3772                    button.setChecked(True)
3773                    break
3774        except ValueError:
3775            pass
3776
3777        self.updateFullModel(context)
3778        self.updateFullPolyModel(context)
3779        self.updateFullMagnetModel(context)
3780
3781    def updateMultiplicityCombo(self, multip):
3782        """
3783        Find and update the multiplicity combobox
3784        """
3785        index = self._model_model.index(self._n_shells_row, 1)
3786        widget = self.lstParams.indexWidget(index)
3787        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3788            widget.setCurrentIndex(widget.findText(str(multip)))
3789        self.current_shell_displayed = multip
3790
3791    def updateFullModel(self, param_dict):
3792        """
3793        Update the model with new parameters
3794        """
3795        assert isinstance(param_dict, dict)
3796        if not dict:
3797            return
3798
3799        def updateFittedValues(row):
3800            # Utility function for main model update
3801            # internal so can use closure for param_dict
3802            param_name = str(self._model_model.item(row, 0).text())
3803            if param_name not in list(param_dict.keys()):
3804                return
3805            # Special case of combo box in the cell (multiplicity)
3806            param_line = param_dict[param_name]
3807            if len(param_line) == 1:
3808                # modify the shells value
3809                try:
3810                    combo_index = int(param_line[0])
3811                except ValueError:
3812                    # quietly pass
3813                    return
3814                index = self._model_model.index(row, 1)
3815                widget = self.lstParams.indexWidget(index)
3816                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3817                    #widget.setCurrentIndex(combo_index)
3818                    return
3819            # checkbox state
3820            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3821            self._model_model.item(row, 0).setCheckState(param_checked)
3822
3823            # parameter value can be either just a value or text on the combobox
3824            param_text = param_dict[param_name][1]
3825            index = self._model_model.index(row, 1)
3826            widget = self.lstParams.indexWidget(index)
3827            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3828                # Find the right index based on text
3829                combo_index = int(param_text, 0)
3830                widget.setCurrentIndex(combo_index)
3831            else:
3832                # modify the param value
3833                param_repr = GuiUtils.formatNumber(param_text, high=True)
3834                self._model_model.item(row, 1).setText(param_repr)
3835
3836            # Potentially the error column
3837            ioffset = 0
3838            joffset = 0
3839            if len(param_dict[param_name])>5:
3840                # error values are not editable - no need to update
3841                ioffset = 1
3842            if self.has_error_column:
3843                joffset = 1
3844            # min/max
3845            try:
3846                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3847                self._model_model.item(row, 2+joffset).setText(param_repr)
3848                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3849                self._model_model.item(row, 3+joffset).setText(param_repr)
3850            except:
3851                pass
3852
3853            # constraints
3854            cons = param_dict[param_name][4+ioffset]
3855            if cons is not None and cons:
3856                value = cons[0]
3857                function = cons[1]
3858                constraint = Constraint()
3859                constraint.value = value
3860                constraint.func = function
3861                self.addConstraintToRow(constraint=constraint, row=row)
3862
3863            self.setFocus()
3864
3865        self.iterateOverModel(updateFittedValues)
3866
3867    def updateFullPolyModel(self, param_dict):
3868        """
3869        Update the polydispersity model with new parameters, create the errors column
3870        """
3871        assert isinstance(param_dict, dict)
3872        if not dict:
3873            return
3874
3875        def updateFittedValues(row):
3876            # Utility function for main model update
3877            # internal so can use closure for param_dict
3878            if row >= self._poly_model.rowCount():
3879                return
3880            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3881            if param_name not in list(param_dict.keys()):
3882                return
3883            # checkbox state
3884            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3885            self._poly_model.item(row,0).setCheckState(param_checked)
3886
3887            # modify the param value
3888            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3889            self._poly_model.item(row, 1).setText(param_repr)
3890
3891            # Potentially the error column
3892            ioffset = 0
3893            joffset = 0
3894            if len(param_dict[param_name])>7:
3895                ioffset = 1
3896            if self.has_poly_error_column:
3897                joffset = 1
3898            # min
3899            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3900            self._poly_model.item(row, 2+joffset).setText(param_repr)
3901            # max
3902            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3903            self._poly_model.item(row, 3+joffset).setText(param_repr)
3904            # Npts
3905            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3906            self._poly_model.item(row, 4+joffset).setText(param_repr)
3907            # Nsigs
3908            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3909            self._poly_model.item(row, 5+joffset).setText(param_repr)
3910
3911            self.setFocus()
3912
3913        self.iterateOverPolyModel(updateFittedValues)
3914
3915    def updateFullMagnetModel(self, param_dict):
3916        """
3917        Update the magnetism model with new parameters, create the errors column
3918        """
3919        assert isinstance(param_dict, dict)
3920        if not dict:
3921            return
3922
3923        def updateFittedValues(row):
3924            # Utility function for main model update
3925            # internal so can use closure for param_dict
3926            if row >= self._magnet_model.rowCount():
3927                return
3928            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3929            if param_name not in list(param_dict.keys()):
3930                return
3931            # checkbox state
3932            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3933            self._magnet_model.item(row,0).setCheckState(param_checked)
3934
3935            # modify the param value
3936            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3937            self._magnet_model.item(row, 1).setText(param_repr)
3938
3939            # Potentially the error column
3940            ioffset = 0
3941            joffset = 0
3942            if len(param_dict[param_name])>4:
3943                ioffset = 1
3944            if self.has_magnet_error_column:
3945                joffset = 1
3946            # min
3947            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3948            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3949            # max
3950            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3951            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3952
3953        self.iterateOverMagnetModel(updateFittedValues)
3954
3955    def getCurrentFitState(self, state=None):
3956        """
3957        Store current state for fit_page
3958        """
3959        # save model option
3960        #if self.model is not None:
3961        #    self.disp_list = self.getDispParamList()
3962        #    state.disp_list = copy.deepcopy(self.disp_list)
3963        #    #state.model = self.model.clone()
3964
3965        # Comboboxes
3966        state.categorycombobox = self.cbCategory.currentText()
3967        state.formfactorcombobox = self.cbModel.currentText()
3968        if self.cbStructureFactor.isEnabled():
3969            state.structurecombobox = self.cbStructureFactor.currentText()
3970        state.tcChi = self.chi2
3971
3972        state.enable2D = self.is2D
3973
3974        #state.weights = copy.deepcopy(self.weights)
3975        # save data
3976        state.data = copy.deepcopy(self.data)
3977
3978        # save plotting range
3979        state.qmin = self.q_range_min
3980        state.qmax = self.q_range_max
3981        state.npts = self.npts
3982
3983        #    self.state.enable_disp = self.enable_disp.GetValue()
3984        #    self.state.disable_disp = self.disable_disp.GetValue()
3985
3986        #    self.state.enable_smearer = \
3987        #                        copy.deepcopy(self.enable_smearer.GetValue())
3988        #    self.state.disable_smearer = \
3989        #                        copy.deepcopy(self.disable_smearer.GetValue())
3990
3991        #self.state.pinhole_smearer = \
3992        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3993        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3994        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3995        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3996        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3997        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3998
3999        p = self.model_parameters
4000        # save checkbutton state and txtcrtl values
4001        state.parameters = FittingUtilities.getStandardParam(self._model_model)
4002        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
4003
4004        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
4005        #self._copy_parameters_state(self.parameters, self.state.parameters)
4006        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
4007        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
4008
Note: See TracBrowser for help on using the repository browser.