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

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

Don't update the shell parameter which might not be there after backend
multiplication.

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