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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since ec4a143 was ec4a143, checked in by piotr, 5 years ago

Allow magnetism for models with magnetic parameters defined. SASVIEW-1199

  • Property mode set to 100644
File size: 151.1 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5import copy
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10import webbrowser
11
12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
15
16from sasmodels import generate
17from sasmodels import modelinfo
18from sasmodels.sasview_model import load_standard_models
19from sasmodels.sasview_model import MultiplicationModel
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23from sas.sascalc.fit.pagestate import PageState
24
25import sas.qtgui.Utilities.GuiUtils as GuiUtils
26import sas.qtgui.Utilities.LocalConfig as LocalConfig
27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
30from sas.qtgui.Plotting.Plotter import PlotterWidget
31
32from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
33from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
34from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
35
36from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
38from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
39from sas.qtgui.Perspectives.Fitting import FittingUtilities
40from sas.qtgui.Perspectives.Fitting import ModelUtilities
41from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
42from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
43from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
44from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
45from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
46from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
47from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
48from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
49from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
50
51TAB_MAGNETISM = 4
52TAB_POLY = 3
53CATEGORY_DEFAULT = "Choose category..."
54CATEGORY_STRUCTURE = "Structure Factor"
55CATEGORY_CUSTOM = "Plugin Models"
56STRUCTURE_DEFAULT = "None"
57
58DEFAULT_POLYDISP_FUNCTION = 'gaussian'
59
60# CRUFT: remove when new release of sasmodels is available
61# https://github.com/SasView/sasview/pull/181#discussion_r218135162
62from sasmodels.sasview_model import SasviewModel
63if not hasattr(SasviewModel, 'get_weights'):
64    def get_weights(self, name):
65        """
66        Returns the polydispersity distribution for parameter *name* as *value* and *weight* arrays.
67        """
68        # type: (str) -> Tuple(np.ndarray, np.ndarray)
69        _, x, w = self._get_weights(self._model_info.parameters[name])
70        return x, w
71
72    SasviewModel.get_weights = get_weights
73
74logger = logging.getLogger(__name__)
75
76class ToolTippedItemModel(QtGui.QStandardItemModel):
77    """
78    Subclass from QStandardItemModel to allow displaying tooltips in
79    QTableView model.
80    """
81    def __init__(self, parent=None):
82        QtGui.QStandardItemModel.__init__(self, parent)
83
84    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
85        """
86        Displays tooltip for each column's header
87        :param section:
88        :param orientation:
89        :param role:
90        :return:
91        """
92        if role == QtCore.Qt.ToolTipRole:
93            if orientation == QtCore.Qt.Horizontal:
94                return str(self.header_tooltips[section])
95
96        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
97
98class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
99    """
100    Main widget for selecting form and structure factor models
101    """
102    constraintAddedSignal = QtCore.pyqtSignal(list)
103    newModelSignal = QtCore.pyqtSignal()
104    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
105    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
106    Calc1DFinishedSignal = QtCore.pyqtSignal(dict)
107    Calc2DFinishedSignal = QtCore.pyqtSignal(dict)
108
109    MAGNETIC_MODELS = ['sphere', 'core_shell_sphere', 'core_multi_shell', 'cylinder', 'parallelepiped']
110
111    def __init__(self, parent=None, data=None, tab_id=1):
112
113        super(FittingWidget, self).__init__()
114
115        # Necessary globals
116        self.parent = parent
117
118        # Which tab is this widget displayed in?
119        self.tab_id = tab_id
120
121        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        has_params = False
1013        if self.kernel_module:
1014            has_mag_params = len(self.kernel_module.magnetic_params) > 0
1015        return self.is2D and has_mag_params
1016
1017    def onSelectModel(self):
1018        """
1019        Respond to select Model from list event
1020        """
1021        model = self.cbModel.currentText()
1022
1023        # Assure the control is active
1024        if not self.cbModel.isEnabled():
1025            return
1026        # Empty combobox forced to be read
1027        if not model:
1028            return
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        # Enable magnetism checkbox for selected models
1167        self.chkMagnetism.setEnabled(self.canHaveMagnetism())
1168        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked() and self.canHaveMagnetism())
1169
1170        # Update column widths
1171        for column, width in self.lstParamHeaderSizes.items():
1172            self.lstParams.setColumnWidth(column, width)
1173
1174        # Update plot
1175        self.updateData()
1176
1177        # Update state stack
1178        self.updateUndo()
1179
1180        # Let others know
1181        self.newModelSignal.emit()
1182
1183    def onSelectCategory(self):
1184        """
1185        Select Category from list
1186        """
1187        category = self.cbCategory.currentText()
1188        # Check if the user chose "Choose category entry"
1189        if category == CATEGORY_DEFAULT:
1190            # if the previous category was not the default, keep it.
1191            # Otherwise, just return
1192            if self._previous_category_index != 0:
1193                # We need to block signals, or else state changes on perceived unchanged conditions
1194                self.cbCategory.blockSignals(True)
1195                self.cbCategory.setCurrentIndex(self._previous_category_index)
1196                self.cbCategory.blockSignals(False)
1197            return
1198
1199        if category == CATEGORY_STRUCTURE:
1200            self.disableModelCombo()
1201            self.enableStructureCombo()
1202            # set the index to 0
1203            self.cbStructureFactor.setCurrentIndex(0)
1204            self.model_parameters = None
1205            self._model_model.clear()
1206            return
1207
1208        # Safely clear and enable the model combo
1209        self.cbModel.blockSignals(True)
1210        self.cbModel.clear()
1211        self.cbModel.blockSignals(False)
1212        self.enableModelCombo()
1213        self.disableStructureCombo()
1214
1215        self._previous_category_index = self.cbCategory.currentIndex()
1216        # Retrieve the list of models
1217        model_list = self.master_category_dict[category]
1218        # Populate the models combobox
1219        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1220
1221    def onPolyModelChange(self, top, bottom):
1222        """
1223        Callback method for updating the main model and sasmodel
1224        parameters with the GUI values in the polydispersity view
1225        """
1226        item = self._poly_model.itemFromIndex(top)
1227        model_column = item.column()
1228        model_row = item.row()
1229        name_index = self._poly_model.index(model_row, 0)
1230        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1231        if "istribution of" in parameter_name:
1232            # just the last word
1233            parameter_name = parameter_name.rsplit()[-1]
1234
1235        delegate = self.lstPoly.itemDelegate()
1236
1237        # Extract changed value.
1238        if model_column == delegate.poly_parameter:
1239            # Is the parameter checked for fitting?
1240            value = item.checkState()
1241            parameter_name = parameter_name + '.width'
1242            if value == QtCore.Qt.Checked:
1243                self.poly_params_to_fit.append(parameter_name)
1244            else:
1245                if parameter_name in self.poly_params_to_fit:
1246                    self.poly_params_to_fit.remove(parameter_name)
1247            self.cmdFit.setEnabled(self.haveParamsToFit())
1248
1249        elif model_column in [delegate.poly_min, delegate.poly_max]:
1250            try:
1251                value = GuiUtils.toDouble(item.text())
1252            except TypeError:
1253                # Can't be converted properly, bring back the old value and exit
1254                return
1255
1256            current_details = self.kernel_module.details[parameter_name]
1257            if self.has_poly_error_column:
1258                # err column changes the indexing
1259                current_details[model_column-2] = value
1260            else:
1261                current_details[model_column-1] = value
1262
1263        elif model_column == delegate.poly_function:
1264            # name of the function - just pass
1265            pass
1266
1267        else:
1268            try:
1269                value = GuiUtils.toDouble(item.text())
1270            except TypeError:
1271                # Can't be converted properly, bring back the old value and exit
1272                return
1273
1274            # Update the sasmodel
1275            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
1276            #self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1277            key = parameter_name + '.' + delegate.columnDict()[model_column]
1278            self.poly_params[key] = value
1279
1280            # Update plot
1281            self.updateData()
1282
1283        # update in param model
1284        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1285            row = self.getRowFromName(parameter_name)
1286            param_item = self._model_model.item(row).child(0).child(0, model_column)
1287            if param_item is None:
1288                return
1289            self._model_model.blockSignals(True)
1290            param_item.setText(item.text())
1291            self._model_model.blockSignals(False)
1292
1293    def onMagnetModelChange(self, top, bottom):
1294        """
1295        Callback method for updating the sasmodel magnetic parameters with the GUI values
1296        """
1297        item = self._magnet_model.itemFromIndex(top)
1298        model_column = item.column()
1299        model_row = item.row()
1300        name_index = self._magnet_model.index(model_row, 0)
1301        parameter_name = str(self._magnet_model.data(name_index))
1302
1303        if model_column == 0:
1304            value = item.checkState()
1305            if value == QtCore.Qt.Checked:
1306                self.magnet_params_to_fit.append(parameter_name)
1307            else:
1308                if parameter_name in self.magnet_params_to_fit:
1309                    self.magnet_params_to_fit.remove(parameter_name)
1310            self.cmdFit.setEnabled(self.haveParamsToFit())
1311            # Update state stack
1312            self.updateUndo()
1313            return
1314
1315        # Extract changed value.
1316        try:
1317            value = GuiUtils.toDouble(item.text())
1318        except TypeError:
1319            # Unparsable field
1320            return
1321        delegate = self.lstMagnetic.itemDelegate()
1322
1323        if model_column > 1:
1324            if model_column == delegate.mag_min:
1325                pos = 1
1326            elif model_column == delegate.mag_max:
1327                pos = 2
1328            elif model_column == delegate.mag_unit:
1329                pos = 0
1330            else:
1331                raise AttributeError("Wrong column in magnetism table.")
1332            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1333            self.kernel_module.details[parameter_name][pos] = value
1334        else:
1335            self.magnet_params[parameter_name] = value
1336            #self.kernel_module.setParam(parameter_name) = value
1337            # Force the chart update when actual parameters changed
1338            self.recalculatePlotData()
1339
1340        # Update state stack
1341        self.updateUndo()
1342
1343    def onHelp(self):
1344        """
1345        Show the "Fitting" section of help
1346        """
1347        tree_location = "/user/qtgui/Perspectives/Fitting/"
1348
1349        # Actual file will depend on the current tab
1350        tab_id = self.tabFitting.currentIndex()
1351        helpfile = "fitting.html"
1352        if tab_id == 0:
1353            helpfile = "fitting_help.html"
1354        elif tab_id == 1:
1355            helpfile = "residuals_help.html"
1356        elif tab_id == 2:
1357            helpfile = "resolution.html"
1358        elif tab_id == 3:
1359            helpfile = "pd/polydispersity.html"
1360        elif tab_id == 4:
1361            helpfile = "magnetism/magnetism.html"
1362        help_location = tree_location + helpfile
1363
1364        self.showHelp(help_location)
1365
1366    def showHelp(self, url):
1367        """
1368        Calls parent's method for opening an HTML page
1369        """
1370        self.parent.showHelp(url)
1371
1372    def onDisplayMagneticAngles(self):
1373        """
1374        Display a simple image showing direction of magnetic angles
1375        """
1376        self.magneticAnglesWidget.show()
1377
1378    def onFit(self):
1379        """
1380        Perform fitting on the current data
1381        """
1382        if self.fit_started:
1383            self.stopFit()
1384            return
1385
1386        # initialize fitter constants
1387        fit_id = 0
1388        handler = None
1389        batch_inputs = {}
1390        batch_outputs = {}
1391        #---------------------------------
1392        if LocalConfig.USING_TWISTED:
1393            handler = None
1394            updater = None
1395        else:
1396            handler = ConsoleUpdate(parent=self.parent,
1397                                    manager=self,
1398                                    improvement_delta=0.1)
1399            updater = handler.update_fit
1400
1401        # Prepare the fitter object
1402        try:
1403            fitters, _ = self.prepareFitters()
1404        except ValueError as ex:
1405            # This should not happen! GUI explicitly forbids this situation
1406            self.communicate.statusBarUpdateSignal.emit(str(ex))
1407            return
1408
1409        # keep local copy of kernel parameters, as they will change during the update
1410        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1411
1412        # Create the fitting thread, based on the fitter
1413        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1414
1415        self.calc_fit = FitThread(handler=handler,
1416                            fn=fitters,
1417                            batch_inputs=batch_inputs,
1418                            batch_outputs=batch_outputs,
1419                            page_id=[[self.page_id]],
1420                            updatefn=updater,
1421                            completefn=completefn,
1422                            reset_flag=self.is_chain_fitting)
1423
1424        if LocalConfig.USING_TWISTED:
1425            # start the trhrhread with twisted
1426            calc_thread = threads.deferToThread(self.calc_fit.compute)
1427            calc_thread.addCallback(completefn)
1428            calc_thread.addErrback(self.fitFailed)
1429        else:
1430            # Use the old python threads + Queue
1431            self.calc_fit.queue()
1432            self.calc_fit.ready(2.5)
1433
1434        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1435        self.fit_started = True
1436
1437        # Disable some elements
1438        self.disableInteractiveElements()
1439
1440    def stopFit(self):
1441        """
1442        Attempt to stop the fitting thread
1443        """
1444        if self.calc_fit is None or not self.calc_fit.isrunning():
1445            return
1446        self.calc_fit.stop()
1447        #re-enable the Fit button
1448        self.enableInteractiveElements()
1449
1450        msg = "Fitting cancelled."
1451        self.communicate.statusBarUpdateSignal.emit(msg)
1452
1453    def updateFit(self):
1454        """
1455        """
1456        print("UPDATE FIT")
1457        pass
1458
1459    def fitFailed(self, reason):
1460        """
1461        """
1462        self.enableInteractiveElements()
1463        msg = "Fitting failed with: "+ str(reason)
1464        self.communicate.statusBarUpdateSignal.emit(msg)
1465
1466    def batchFittingCompleted(self, result):
1467        """
1468        Send the finish message from calculate threads to main thread
1469        """
1470        if result is None:
1471            result = tuple()
1472        self.batchFittingFinishedSignal.emit(result)
1473
1474    def batchFitComplete(self, result):
1475        """
1476        Receive and display batch fitting results
1477        """
1478        #re-enable the Fit button
1479        self.enableInteractiveElements()
1480
1481        if len(result) == 0:
1482            msg = "Fitting failed."
1483            self.communicate.statusBarUpdateSignal.emit(msg)
1484            return
1485
1486        # Show the grid panel
1487        self.communicate.sendDataToGridSignal.emit(result[0])
1488
1489        elapsed = result[1]
1490        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1491        self.communicate.statusBarUpdateSignal.emit(msg)
1492
1493        # Run over the list of results and update the items
1494        for res_index, res_list in enumerate(result[0]):
1495            # results
1496            res = res_list[0]
1497            param_dict = self.paramDictFromResults(res)
1498
1499            # create local kernel_module
1500            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1501            # pull out current data
1502            data = self._logic[res_index].data
1503
1504            # Switch indexes
1505            self.onSelectBatchFilename(res_index)
1506
1507            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1508            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1509
1510        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1511        if self.kernel_module is not None:
1512            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1513
1514    def paramDictFromResults(self, results):
1515        """
1516        Given the fit results structure, pull out optimized parameters and return them as nicely
1517        formatted dict
1518        """
1519        if results.fitness is None or \
1520            not np.isfinite(results.fitness) or \
1521            np.any(results.pvec is None) or \
1522            not np.all(np.isfinite(results.pvec)):
1523            msg = "Fitting did not converge!"
1524            self.communicate.statusBarUpdateSignal.emit(msg)
1525            msg += results.mesg
1526            logger.error(msg)
1527            return
1528
1529        param_list = results.param_list # ['radius', 'radius.width']
1530        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1531        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1532        params_and_errors = list(zip(param_values, param_stderr))
1533        param_dict = dict(zip(param_list, params_and_errors))
1534
1535        return param_dict
1536
1537    def fittingCompleted(self, result):
1538        """
1539        Send the finish message from calculate threads to main thread
1540        """
1541        if result is None:
1542            result = tuple()
1543        self.fittingFinishedSignal.emit(result)
1544
1545    def fitComplete(self, result):
1546        """
1547        Receive and display fitting results
1548        "result" is a tuple of actual result list and the fit time in seconds
1549        """
1550        #re-enable the Fit button
1551        self.enableInteractiveElements()
1552
1553        if len(result) == 0:
1554            msg = "Fitting failed."
1555            self.communicate.statusBarUpdateSignal.emit(msg)
1556            return
1557
1558        res_list = result[0][0]
1559        res = res_list[0]
1560        self.chi2 = res.fitness
1561        param_dict = self.paramDictFromResults(res)
1562
1563        if param_dict is None:
1564            return
1565        if hasattr(res, 'convergence') and len(res.convergence)>0:
1566            self.communicate.resultPlotUpdateSignal.emit(result[0])
1567
1568        elapsed = result[1]
1569        if self.calc_fit is not None and self.calc_fit._interrupting:
1570            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1571            logger.warning("\n"+msg+"\n")
1572        else:
1573            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1574        self.communicate.statusBarUpdateSignal.emit(msg)
1575
1576        # Dictionary of fitted parameter: value, error
1577        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1578        self.updateModelFromList(param_dict)
1579
1580        self.updatePolyModelFromList(param_dict)
1581
1582        self.updateMagnetModelFromList(param_dict)
1583
1584        # update charts
1585        self.onPlot()
1586        #self.recalculatePlotData()
1587
1588
1589        # Read only value - we can get away by just printing it here
1590        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1591        self.lblChi2Value.setText(chi2_repr)
1592
1593    def prepareFitters(self, fitter=None, fit_id=0):
1594        """
1595        Prepare the Fitter object for use in fitting
1596        """
1597        # fitter = None -> single/batch fitting
1598        # fitter = Fit() -> simultaneous fitting
1599
1600        # Data going in
1601        data = self.logic.data
1602        model = copy.deepcopy(self.kernel_module)
1603        qmin = self.q_range_min
1604        qmax = self.q_range_max
1605        # add polydisperse/magnet parameters if asked
1606        self.updateKernelModelWithExtraParams(model)
1607
1608        params_to_fit = copy.deepcopy(self.main_params_to_fit)
1609        if self.chkPolydispersity.isChecked():
1610            params_to_fit += self.poly_params_to_fit
1611        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
1612            params_to_fit += self.magnet_params_to_fit
1613        if not params_to_fit:
1614            raise ValueError('Fitting requires at least one parameter to optimize.')
1615
1616        # Get the constraints.
1617        constraints = self.getComplexConstraintsForModel()
1618        if fitter is None:
1619            # For single fits - check for inter-model constraints
1620            constraints = self.getConstraintsForFitting()
1621
1622        smearer = self.smearing_widget.smearer()
1623        handler = None
1624        batch_inputs = {}
1625        batch_outputs = {}
1626
1627        fitters = []
1628        for fit_index in self.all_data:
1629            fitter_single = Fit() if fitter is None else fitter
1630            data = GuiUtils.dataFromItem(fit_index)
1631            # Potential weights added directly to data
1632            weighted_data = self.addWeightingToData(data)
1633            try:
1634                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1635                             constraints=constraints)
1636            except ValueError as ex:
1637                raise ValueError("Setting model parameters failed with: %s" % ex)
1638
1639            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1640            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1641                            qmax=qmax)
1642            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1643            if fitter is None:
1644                # Assign id to the new fitter only
1645                fitter_single.fitter_id = [self.page_id]
1646            fit_id += 1
1647            fitters.append(fitter_single)
1648
1649        return fitters, fit_id
1650
1651    def iterateOverModel(self, func):
1652        """
1653        Take func and throw it inside the model row loop
1654        """
1655        for row_i in range(self._model_model.rowCount()):
1656            func(row_i)
1657
1658    def updateModelFromList(self, param_dict):
1659        """
1660        Update the model with new parameters, create the errors column
1661        """
1662        assert isinstance(param_dict, dict)
1663        if not dict:
1664            return
1665
1666        def updateFittedValues(row):
1667            # Utility function for main model update
1668            # internal so can use closure for param_dict
1669            param_name = str(self._model_model.item(row, 0).text())
1670            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1671                return
1672            # modify the param value
1673            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1674            self._model_model.item(row, 1).setText(param_repr)
1675            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1676            if self.has_error_column:
1677                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1678                self._model_model.item(row, 2).setText(error_repr)
1679
1680        def updatePolyValues(row):
1681            # Utility function for updateof polydispersity part of the main model
1682            param_name = str(self._model_model.item(row, 0).text())+'.width'
1683            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1684                return
1685            # modify the param value
1686            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1687            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1688            # modify the param error
1689            if self.has_error_column:
1690                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1691                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1692
1693        def createErrorColumn(row):
1694            # Utility function for error column update
1695            item = QtGui.QStandardItem()
1696            def createItem(param_name):
1697                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1698                item.setText(error_repr)
1699            def curr_param():
1700                return str(self._model_model.item(row, 0).text())
1701
1702            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1703
1704            error_column.append(item)
1705
1706        def createPolyErrorColumn(row):
1707            # Utility function for error column update in the polydispersity sub-rows
1708            # NOTE: only creates empty items; updatePolyValues adds the error value
1709            item = self._model_model.item(row, 0)
1710            if not item.hasChildren():
1711                return
1712            poly_item = item.child(0)
1713            if not poly_item.hasChildren():
1714                return
1715            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1716
1717        if not self.has_error_column:
1718            # create top-level error column
1719            error_column = []
1720            self.lstParams.itemDelegate().addErrorColumn()
1721            self.iterateOverModel(createErrorColumn)
1722
1723            self._model_model.insertColumn(2, error_column)
1724
1725            FittingUtilities.addErrorHeadersToModel(self._model_model)
1726
1727            # create error column in polydispersity sub-rows
1728            self.iterateOverModel(createPolyErrorColumn)
1729
1730            self.has_error_column = True
1731
1732        # block signals temporarily, so we don't end up
1733        # updating charts with every single model change on the end of fitting
1734        self._model_model.dataChanged.disconnect()
1735        self.iterateOverModel(updateFittedValues)
1736        self.iterateOverModel(updatePolyValues)
1737        self._model_model.dataChanged.connect(self.onMainParamsChange)
1738
1739    def iterateOverPolyModel(self, func):
1740        """
1741        Take func and throw it inside the poly model row loop
1742        """
1743        for row_i in range(self._poly_model.rowCount()):
1744            func(row_i)
1745
1746    def updatePolyModelFromList(self, param_dict):
1747        """
1748        Update the polydispersity model with new parameters, create the errors column
1749        """
1750        assert isinstance(param_dict, dict)
1751        if not dict:
1752            return
1753
1754        def updateFittedValues(row_i):
1755            # Utility function for main model update
1756            # internal so can use closure for param_dict
1757            if row_i >= self._poly_model.rowCount():
1758                return
1759            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1760            if param_name not in list(param_dict.keys()):
1761                return
1762            # modify the param value
1763            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1764            self._poly_model.item(row_i, 1).setText(param_repr)
1765            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1766            if self.has_poly_error_column:
1767                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1768                self._poly_model.item(row_i, 2).setText(error_repr)
1769
1770        def createErrorColumn(row_i):
1771            # Utility function for error column update
1772            if row_i >= self._poly_model.rowCount():
1773                return
1774            item = QtGui.QStandardItem()
1775
1776            def createItem(param_name):
1777                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1778                item.setText(error_repr)
1779
1780            def poly_param():
1781                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1782
1783            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1784
1785            error_column.append(item)
1786
1787        # block signals temporarily, so we don't end up
1788        # updating charts with every single model change on the end of fitting
1789        self._poly_model.dataChanged.disconnect()
1790        self.iterateOverPolyModel(updateFittedValues)
1791        self._poly_model.dataChanged.connect(self.onPolyModelChange)
1792
1793        if self.has_poly_error_column:
1794            return
1795
1796        self.lstPoly.itemDelegate().addErrorColumn()
1797        error_column = []
1798        self.iterateOverPolyModel(createErrorColumn)
1799
1800        # switch off reponse to model change
1801        self._poly_model.insertColumn(2, error_column)
1802        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1803
1804        self.has_poly_error_column = True
1805
1806    def iterateOverMagnetModel(self, func):
1807        """
1808        Take func and throw it inside the magnet model row loop
1809        """
1810        for row_i in range(self._magnet_model.rowCount()):
1811            func(row_i)
1812
1813    def updateMagnetModelFromList(self, param_dict):
1814        """
1815        Update the magnetic model with new parameters, create the errors column
1816        """
1817        assert isinstance(param_dict, dict)
1818        if not dict:
1819            return
1820        if self._magnet_model.rowCount() == 0:
1821            return
1822
1823        def updateFittedValues(row):
1824            # Utility function for main model update
1825            # internal so can use closure for param_dict
1826            if self._magnet_model.item(row, 0) is None:
1827                return
1828            param_name = str(self._magnet_model.item(row, 0).text())
1829            if param_name not in list(param_dict.keys()):
1830                return
1831            # modify the param value
1832            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1833            self._magnet_model.item(row, 1).setText(param_repr)
1834            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1835            if self.has_magnet_error_column:
1836                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1837                self._magnet_model.item(row, 2).setText(error_repr)
1838
1839        def createErrorColumn(row):
1840            # Utility function for error column update
1841            item = QtGui.QStandardItem()
1842            def createItem(param_name):
1843                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1844                item.setText(error_repr)
1845            def curr_param():
1846                return str(self._magnet_model.item(row, 0).text())
1847
1848            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1849
1850            error_column.append(item)
1851
1852        # block signals temporarily, so we don't end up
1853        # updating charts with every single model change on the end of fitting
1854        self._magnet_model.dataChanged.disconnect()
1855        self.iterateOverMagnetModel(updateFittedValues)
1856        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
1857
1858        if self.has_magnet_error_column:
1859            return
1860
1861        self.lstMagnetic.itemDelegate().addErrorColumn()
1862        error_column = []
1863        self.iterateOverMagnetModel(createErrorColumn)
1864
1865        # switch off reponse to model change
1866        self._magnet_model.insertColumn(2, error_column)
1867        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1868
1869        self.has_magnet_error_column = True
1870
1871    def onPlot(self):
1872        """
1873        Plot the current set of data
1874        """
1875        # Regardless of previous state, this should now be `plot show` functionality only
1876        self.cmdPlot.setText("Show Plot")
1877        # Force data recalculation so existing charts are updated
1878        if not self.data_is_loaded:
1879            self.showTheoryPlot()
1880        else:
1881            self.showPlot()
1882        # This is an important processEvent.
1883        # This allows charts to be properly updated in order
1884        # of plots being applied.
1885        QtWidgets.QApplication.processEvents()
1886        self.recalculatePlotData() # recalc+plot theory again (2nd)
1887
1888    def onSmearingOptionsUpdate(self):
1889        """
1890        React to changes in the smearing widget
1891        """
1892        self.calculateQGridForModel()
1893
1894    def recalculatePlotData(self):
1895        """
1896        Generate a new dataset for model
1897        """
1898        if not self.data_is_loaded:
1899            self.createDefaultDataset()
1900        self.calculateQGridForModel()
1901
1902    def showTheoryPlot(self):
1903        """
1904        Show the current theory plot in MPL
1905        """
1906        # Show the chart if ready
1907        if self.theory_item is None:
1908            self.recalculatePlotData()
1909        elif self.model_data:
1910            self._requestPlots(self.model_data.filename, self.theory_item.model())
1911
1912    def showPlot(self):
1913        """
1914        Show the current plot in MPL
1915        """
1916        # Show the chart if ready
1917        data_to_show = self.data
1918        # Any models for this page
1919        current_index = self.all_data[self.data_index]
1920        item = self._requestPlots(self.data.filename, current_index.model())
1921        if item:
1922            # fit+data has not been shown - show just data
1923            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
1924
1925    def _requestPlots(self, item_name, item_model):
1926        """
1927        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
1928        """
1929        fitpage_name = "" if self.tab_id is None else "M"+str(self.tab_id)
1930        plots = GuiUtils.plotsFromFilename(item_name, item_model)
1931        # Has the fitted data been shown?
1932        data_shown = False
1933        item = None
1934        for item, plot in plots.items():
1935            if fitpage_name in plot.name:
1936                data_shown = True
1937                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1938        # return the last data item seen, if nothing was plotted; supposed to be just data)
1939        return None if data_shown else item
1940
1941    def onOptionsUpdate(self):
1942        """
1943        Update local option values and replot
1944        """
1945        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1946            self.options_widget.state()
1947        # set Q range labels on the main tab
1948        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
1949        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
1950        self.recalculatePlotData()
1951
1952    def setDefaultStructureCombo(self):
1953        """
1954        Fill in the structure factors combo box with defaults
1955        """
1956        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1957        factors = [factor[0] for factor in structure_factor_list]
1958        factors.insert(0, STRUCTURE_DEFAULT)
1959        self.cbStructureFactor.clear()
1960        self.cbStructureFactor.addItems(sorted(factors))
1961
1962    def createDefaultDataset(self):
1963        """
1964        Generate default Dataset 1D/2D for the given model
1965        """
1966        # Create default datasets if no data passed
1967        if self.is2D:
1968            qmax = self.q_range_max/np.sqrt(2)
1969            qstep = self.npts
1970            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1971            return
1972        elif self.log_points:
1973            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1974            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1975            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1976        else:
1977            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1978                                   num=self.npts, endpoint=True)
1979        self.logic.createDefault1dData(interval, self.tab_id)
1980
1981    def readCategoryInfo(self):
1982        """
1983        Reads the categories in from file
1984        """
1985        self.master_category_dict = defaultdict(list)
1986        self.by_model_dict = defaultdict(list)
1987        self.model_enabled_dict = defaultdict(bool)
1988
1989        categorization_file = CategoryInstaller.get_user_file()
1990        if not os.path.isfile(categorization_file):
1991            categorization_file = CategoryInstaller.get_default_file()
1992        with open(categorization_file, 'rb') as cat_file:
1993            self.master_category_dict = json.load(cat_file)
1994            self.regenerateModelDict()
1995
1996        # Load the model dict
1997        models = load_standard_models()
1998        for model in models:
1999            self.models[model.name] = model
2000
2001        self.readCustomCategoryInfo()
2002
2003    def readCustomCategoryInfo(self):
2004        """
2005        Reads the custom model category
2006        """
2007        #Looking for plugins
2008        self.plugins = list(self.custom_models.values())
2009        plugin_list = []
2010        for name, plug in self.custom_models.items():
2011            self.models[name] = plug
2012            plugin_list.append([name, True])
2013        if plugin_list:
2014            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
2015
2016    def regenerateModelDict(self):
2017        """
2018        Regenerates self.by_model_dict which has each model name as the
2019        key and the list of categories belonging to that model
2020        along with the enabled mapping
2021        """
2022        self.by_model_dict = defaultdict(list)
2023        for category in self.master_category_dict:
2024            for (model, enabled) in self.master_category_dict[category]:
2025                self.by_model_dict[model].append(category)
2026                self.model_enabled_dict[model] = enabled
2027
2028    def addBackgroundToModel(self, model):
2029        """
2030        Adds background parameter with default values to the model
2031        """
2032        assert isinstance(model, QtGui.QStandardItemModel)
2033        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2034        FittingUtilities.addCheckedListToModel(model, checked_list)
2035        last_row = model.rowCount()-1
2036        model.item(last_row, 0).setEditable(False)
2037        model.item(last_row, 4).setEditable(False)
2038
2039    def addScaleToModel(self, model):
2040        """
2041        Adds scale parameter with default values to the model
2042        """
2043        assert isinstance(model, QtGui.QStandardItemModel)
2044        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2045        FittingUtilities.addCheckedListToModel(model, checked_list)
2046        last_row = model.rowCount()-1
2047        model.item(last_row, 0).setEditable(False)
2048        model.item(last_row, 4).setEditable(False)
2049
2050    def addWeightingToData(self, data):
2051        """
2052        Adds weighting contribution to fitting data
2053        """
2054        new_data = copy.deepcopy(data)
2055        # Send original data for weighting
2056        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2057        if self.is2D:
2058            new_data.err_data = weight
2059        else:
2060            new_data.dy = weight
2061
2062        return new_data
2063
2064    def updateQRange(self):
2065        """
2066        Updates Q Range display
2067        """
2068        if self.data_is_loaded:
2069            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2070        # set Q range labels on the main tab
2071        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2072        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
2073        # set Q range labels on the options tab
2074        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2075
2076    def SASModelToQModel(self, model_name, structure_factor=None):
2077        """
2078        Setting model parameters into table based on selected category
2079        """
2080        # Crete/overwrite model items
2081        self._model_model.clear()
2082        self._poly_model.clear()
2083        self._magnet_model.clear()
2084
2085        if model_name is None:
2086            if structure_factor not in (None, "None"):
2087                # S(Q) on its own, treat the same as a form factor
2088                self.kernel_module = None
2089                self.fromStructureFactorToQModel(structure_factor)
2090            else:
2091                # No models selected
2092                return
2093        else:
2094            self.fromModelToQModel(model_name)
2095            self.addExtraShells()
2096
2097            # Allow the SF combobox visibility for the given sasmodel
2098            self.enableStructureFactorControl(structure_factor)
2099       
2100            # Add S(Q)
2101            if self.cbStructureFactor.isEnabled():
2102                structure_factor = self.cbStructureFactor.currentText()
2103                self.fromStructureFactorToQModel(structure_factor)
2104
2105            # Add polydispersity to the model
2106            self.poly_params = {}
2107            self.setPolyModel()
2108            # Add magnetic parameters to the model
2109            self.magnet_params = {}
2110            self.setMagneticModel()
2111
2112        # Now we claim the model has been loaded
2113        self.model_is_loaded = True
2114        # Change the model name to a monicker
2115        self.kernel_module.name = self.modelName()
2116        # Update the smearing tab
2117        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2118
2119        # (Re)-create headers
2120        FittingUtilities.addHeadersToModel(self._model_model)
2121        self.lstParams.header().setFont(self.boldFont)
2122
2123        # Update Q Ranges
2124        self.updateQRange()
2125
2126    def fromModelToQModel(self, model_name):
2127        """
2128        Setting model parameters into QStandardItemModel based on selected _model_
2129        """
2130        name = model_name
2131        kernel_module = None
2132        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2133            # custom kernel load requires full path
2134            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2135        try:
2136            kernel_module = generate.load_kernel_module(name)
2137        except ModuleNotFoundError as ex:
2138            pass
2139        except FileNotFoundError as ex:
2140            # can happen when name attribute not the same as actual filename
2141            pass
2142
2143        if kernel_module is None:
2144            # mismatch between "name" attribute and actual filename.
2145            curr_model = self.models[model_name]
2146            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2147            try:
2148                kernel_module = generate.load_kernel_module(name)
2149            except ModuleNotFoundError as ex:
2150                logger.error("Can't find the model "+ str(ex))
2151                return
2152
2153        if hasattr(kernel_module, 'parameters'):
2154            # built-in and custom models
2155            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2156
2157        elif hasattr(kernel_module, 'model_info'):
2158            # for sum/multiply models
2159            self.model_parameters = kernel_module.model_info.parameters
2160
2161        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2162            # this probably won't work if there's no model_info, but just in case
2163            self.model_parameters = kernel_module.Model._model_info.parameters
2164        else:
2165            # no parameters - default to blank table
2166            msg = "No parameters found in model '{}'.".format(model_name)
2167            logger.warning(msg)
2168            self.model_parameters = modelinfo.ParameterTable([])
2169
2170        # Instantiate the current sasmodel
2171        self.kernel_module = self.models[model_name]()
2172
2173        # Change the model name to a monicker
2174        self.kernel_module.name = self.modelName()
2175
2176        # Explicitly add scale and background with default values
2177        temp_undo_state = self.undo_supported
2178        self.undo_supported = False
2179        self.addScaleToModel(self._model_model)
2180        self.addBackgroundToModel(self._model_model)
2181        self.undo_supported = temp_undo_state
2182
2183        self.shell_names = self.shellNamesList()
2184
2185        # Add heading row
2186        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2187
2188        # Update the QModel
2189        FittingUtilities.addParametersToModel(
2190                self.model_parameters,
2191                self.kernel_module,
2192                self.is2D,
2193                self._model_model,
2194                self.lstParams)
2195
2196    def fromStructureFactorToQModel(self, structure_factor):
2197        """
2198        Setting model parameters into QStandardItemModel based on selected _structure factor_
2199        """
2200        if structure_factor is None or structure_factor=="None":
2201            return
2202
2203        product_params = None
2204
2205        if self.kernel_module is None:
2206            # Structure factor is the only selected model; build it and show all its params
2207            self.kernel_module = self.models[structure_factor]()
2208            self.kernel_module.name = self.modelName()
2209            s_params = self.kernel_module._model_info.parameters
2210            s_params_orig = s_params
2211        else:
2212            s_kernel = self.models[structure_factor]()
2213            p_kernel = self.kernel_module
2214            # need to reset multiplicity to get the right product
2215            if p_kernel.is_multiplicity_model:
2216                p_kernel.multiplicity = p_kernel.multiplicity_info.number
2217
2218            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2219            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2220
2221            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2222            # Modify the name to correspond to shown items
2223            self.kernel_module.name = self.modelName()
2224            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2225            all_param_names = [param.name for param in all_params]
2226
2227            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2228            # conflicting names with P(Q) params will cause a rename
2229
2230            if "radius_effective_mode" in all_param_names:
2231                # Show all parameters
2232                # In this case, radius_effective is NOT pruned by sasmodels.product
2233                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2234                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2235                product_params = modelinfo.ParameterTable(
2236                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2237            else:
2238                # Ensure radius_effective is not displayed
2239                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2240                if "radius_effective" in all_param_names:
2241                    # In this case, radius_effective is NOT pruned by sasmodels.product
2242                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2243                    product_params = modelinfo.ParameterTable(
2244                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2245                else:
2246                    # In this case, radius_effective is pruned by sasmodels.product
2247                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2248                    product_params = modelinfo.ParameterTable(
2249                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2250
2251        # Add heading row
2252        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2253
2254        # Get new rows for QModel
2255        # Any renamed parameters are stored as data in the relevant item, for later handling
2256        FittingUtilities.addSimpleParametersToModel(
2257                parameters=s_params,
2258                is2D=self.is2D,
2259                parameters_original=s_params_orig,
2260                model=self._model_model,
2261                view=self.lstParams)
2262
2263        # Insert product-only params into QModel
2264        if product_params:
2265            prod_rows = FittingUtilities.addSimpleParametersToModel(
2266                    parameters=product_params,
2267                    is2D=self.is2D,
2268                    parameters_original=None,
2269                    model=self._model_model,
2270                    view=self.lstParams,
2271                    row_num=2)
2272
2273            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2274            self._n_shells_row += len(prod_rows)
2275
2276    def haveParamsToFit(self):
2277        """
2278        Finds out if there are any parameters ready to be fitted
2279        """
2280        if not self.logic.data_is_loaded:
2281            return False
2282        if self.main_params_to_fit:
2283            return True
2284        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2285            return True
2286        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self.magnet_params_to_fit:
2287            return True
2288        return False
2289
2290    def onMainParamsChange(self, top, bottom):
2291        """
2292        Callback method for updating the sasmodel parameters with the GUI values
2293        """
2294        item = self._model_model.itemFromIndex(top)
2295
2296        model_column = item.column()
2297
2298        if model_column == 0:
2299            self.checkboxSelected(item)
2300            self.cmdFit.setEnabled(self.haveParamsToFit())
2301            # Update state stack
2302            self.updateUndo()
2303            return
2304
2305        model_row = item.row()
2306        name_index = self._model_model.index(model_row, 0)
2307        name_item = self._model_model.itemFromIndex(name_index)
2308
2309        # Extract changed value.
2310        try:
2311            value = GuiUtils.toDouble(item.text())
2312        except TypeError:
2313            # Unparsable field
2314            return
2315
2316        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2317        if name_item.data(QtCore.Qt.UserRole):
2318            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2319        else:
2320            parameter_name = str(self._model_model.data(name_index))
2321
2322        # Update the parameter value - note: this supports +/-inf as well
2323        param_column = self.lstParams.itemDelegate().param_value
2324        min_column = self.lstParams.itemDelegate().param_min
2325        max_column = self.lstParams.itemDelegate().param_max
2326        if model_column == param_column:
2327            # don't try to update multiplicity counters if they aren't there.
2328            # Note that this will fail for proper bad update where the model
2329            # doesn't contain multiplicity parameter
2330            if parameter_name != self.kernel_module.multiplicity_info.control:
2331                self.kernel_module.setParam(parameter_name, value)
2332        elif model_column == min_column:
2333            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2334            self.kernel_module.details[parameter_name][1] = value
2335        elif model_column == max_column:
2336            self.kernel_module.details[parameter_name][2] = value
2337        else:
2338            # don't update the chart
2339            return
2340
2341        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2342        # TODO: multishell params in self.kernel_module.details[??] = value
2343
2344        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2345        # necessary
2346        self.processEffectiveRadius()
2347
2348        # Force the chart update when actual parameters changed
2349        if model_column == 1:
2350            self.recalculatePlotData()
2351
2352        # Update state stack
2353        self.updateUndo()
2354
2355    def processEffectiveRadius(self):
2356        """
2357        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2358        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2359        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2360
2361        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2362        displayed for the user; that is not dealt with here, but in complete1D.
2363        """
2364        ER_row = self.getRowFromName("radius_effective")
2365        if ER_row is None:
2366            return
2367
2368        ER_mode_row = self.getRowFromName("radius_effective_mode")
2369        if ER_mode_row is None:
2370            return
2371
2372        try:
2373            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2374        except ValueError:
2375            logging.error("radius_effective_mode was set to an invalid value.")
2376            return
2377
2378        if ER_mode == 0:
2379            # ensure the ER value can be modified by user
2380            self.setParamEditableByRow(ER_row, True)
2381        elif ER_mode > 0:
2382            # ensure the ER value cannot be modified by user
2383            self.setParamEditableByRow(ER_row, False)
2384        else:
2385            logging.error("radius_effective_mode was set to an invalid value.")
2386
2387    def setParamEditableByRow(self, row, editable=True):
2388        """
2389        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2390        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2391        """
2392        item_name = self._model_model.item(row, 0)
2393        item_value = self._model_model.item(row, 1)
2394
2395        item_value.setEditable(editable)
2396
2397        if editable:
2398            # reset font
2399            item_name.setFont(QtGui.QFont())
2400            # reset colour
2401            item_name.setForeground(QtGui.QBrush())
2402            # make checkable
2403            item_name.setCheckable(True)
2404        else:
2405            # change font
2406            font = QtGui.QFont()
2407            font.setItalic(True)
2408            item_name.setFont(font)
2409            # change colour
2410            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2411            # make not checkable (and uncheck)
2412            item_name.setCheckState(QtCore.Qt.Unchecked)
2413            item_name.setCheckable(False)
2414
2415    def isCheckable(self, row):
2416        return self._model_model.item(row, 0).isCheckable()
2417
2418    def checkboxSelected(self, item):
2419        # Assure we're dealing with checkboxes
2420        if not item.isCheckable():
2421            return
2422        status = item.checkState()
2423
2424        # If multiple rows selected - toggle all of them, filtering uncheckable
2425        # Switch off signaling from the model to avoid recursion
2426        self._model_model.blockSignals(True)
2427        # Convert to proper indices and set requested enablement
2428        self.setParameterSelection(status)
2429        self._model_model.blockSignals(False)
2430
2431        # update the list of parameters to fit
2432        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2433
2434    def checkedListFromModel(self, model):
2435        """
2436        Returns list of checked parameters for given model
2437        """
2438        def isChecked(row):
2439            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2440
2441        return [str(model.item(row_index, 0).text())
2442                for row_index in range(model.rowCount())
2443                if isChecked(row_index)]
2444
2445    def createNewIndex(self, fitted_data):
2446        """
2447        Create a model or theory index with passed Data1D/Data2D
2448        """
2449        if self.data_is_loaded:
2450            if not fitted_data.name:
2451                name = self.nameForFittedData(self.data.filename)
2452                fitted_data.title = name
2453                fitted_data.name = name
2454                fitted_data.filename = name
2455                fitted_data.symbol = "Line"
2456            self.updateModelIndex(fitted_data)
2457        else:
2458            if not fitted_data.name:
2459                name = self.nameForFittedData(self.kernel_module.id)
2460            else:
2461                name = fitted_data.name
2462            fitted_data.title = name
2463            fitted_data.filename = name
2464            fitted_data.symbol = "Line"
2465            self.createTheoryIndex(fitted_data)
2466            # Switch to the theory tab for user's glee
2467            self.communicate.changeDataExplorerTabSignal.emit(1)
2468
2469    def updateModelIndex(self, fitted_data):
2470        """
2471        Update a QStandardModelIndex containing model data
2472        """
2473        name = self.nameFromData(fitted_data)
2474        # Make this a line if no other defined
2475        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2476            fitted_data.symbol = 'Line'
2477        # Notify the GUI manager so it can update the main model in DataExplorer
2478        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2479
2480    def createTheoryIndex(self, fitted_data):
2481        """
2482        Create a QStandardModelIndex containing model data
2483        """
2484        name = self.nameFromData(fitted_data)
2485        # Notify the GUI manager so it can create the theory model in DataExplorer
2486        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2487        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2488
2489    def nameFromData(self, fitted_data):
2490        """
2491        Return name for the dataset. Terribly impure function.
2492        """
2493        if fitted_data.name is None:
2494            name = self.nameForFittedData(self.logic.data.filename)
2495            fitted_data.title = name
2496            fitted_data.name = name
2497            fitted_data.filename = name
2498        else:
2499            name = fitted_data.name
2500        return name
2501
2502    def methodCalculateForData(self):
2503        '''return the method for data calculation'''
2504        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2505
2506    def methodCompleteForData(self):
2507        '''return the method for result parsin on calc complete '''
2508        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2509
2510    def updateKernelModelWithExtraParams(self, model=None):
2511        """
2512        Updates kernel model 'model' with extra parameters from
2513        the polydisp and magnetism tab, if the tabs are enabled
2514        """
2515        if model is None: return
2516        if not hasattr(model, 'setParam'): return
2517
2518        # add polydisperse parameters if asked
2519        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2520            for key, value in self.poly_params.items():
2521                model.setParam(key, value)
2522        # add magnetic params if asked
2523        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
2524            for key, value in self.magnet_params.items():
2525                model.setParam(key, value)
2526
2527    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2528        """
2529        Wrapper for Calc1D/2D calls
2530        """
2531        if data is None:
2532            data = self.data
2533        if model is None:
2534            model = copy.deepcopy(self.kernel_module)
2535            self.updateKernelModelWithExtraParams(model)
2536
2537        if completefn is None:
2538            completefn = self.methodCompleteForData()
2539        smearer = self.smearing_widget.smearer()
2540        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2541
2542        # Disable buttons/table
2543        self.disableInteractiveElements()
2544        # Awful API to a backend method.
2545        calc_thread = self.methodCalculateForData()(data=data,
2546                                               model=model,
2547                                               page_id=0,
2548                                               qmin=self.q_range_min,
2549                                               qmax=self.q_range_max,
2550                                               smearer=smearer,
2551                                               state=None,
2552                                               weight=weight,
2553                                               fid=None,
2554                                               toggle_mode_on=False,
2555                                               completefn=completefn,
2556                                               update_chisqr=True,
2557                                               exception_handler=self.calcException,
2558                                               source=None)
2559        if use_threads:
2560            if LocalConfig.USING_TWISTED:
2561                # start the thread with twisted
2562                thread = threads.deferToThread(calc_thread.compute)
2563                thread.addCallback(completefn)
2564                thread.addErrback(self.calculateDataFailed)
2565            else:
2566                # Use the old python threads + Queue
2567                calc_thread.queue()
2568                calc_thread.ready(2.5)
2569        else:
2570            results = calc_thread.compute()
2571            completefn(results)
2572
2573    def calculateQGridForModel(self):
2574        """
2575        Prepare the fitting data object, based on current ModelModel
2576        """
2577        if self.kernel_module is None:
2578            return
2579        self.calculateQGridForModelExt()
2580
2581    def calculateDataFailed(self, reason):
2582        """
2583        Thread returned error
2584        """
2585        # Bring the GUI to normal state
2586        self.enableInteractiveElements()
2587        print("Calculate Data failed with ", reason)
2588
2589    def completed1D(self, return_data):
2590        self.Calc1DFinishedSignal.emit(return_data)
2591
2592    def completed2D(self, return_data):
2593        self.Calc2DFinishedSignal.emit(return_data)
2594
2595    def complete1D(self, return_data):
2596        """
2597        Plot the current 1D data
2598        """
2599        # Bring the GUI to normal state
2600        self.enableInteractiveElements()
2601        if return_data is None:
2602            return
2603        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2604
2605        # assure the current index is set properly for batch
2606        if len(self._logic) > 1:
2607            for i, logic in enumerate(self._logic):
2608                if logic.data.name in fitted_data.name:
2609                    self.data_index = i
2610
2611        residuals = self.calculateResiduals(fitted_data)
2612        self.model_data = fitted_data
2613        new_plots = [fitted_data]
2614        if residuals is not None:
2615            new_plots.append(residuals)
2616
2617        if self.data_is_loaded:
2618            # delete any plots associated with the data that were not updated
2619            # (e.g. to remove beta(Q), S_eff(Q))
2620            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2621            pass
2622        else:
2623            # delete theory items for the model, in order to get rid of any
2624            # redundant items, e.g. beta(Q), S_eff(Q)
2625            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2626
2627        # Create plots for parameters with enabled polydispersity
2628        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2629            data_id = fitted_data.id.split()
2630            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2631            data_name = fitted_data.name.split()
2632            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2633            self.createNewIndex(plot)
2634            new_plots.append(plot)
2635
2636        # Create plots for intermediate product data
2637        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2638        for plot in plots:
2639            plot.symbol = "Line"
2640            self.createNewIndex(plot)
2641            new_plots.append(plot)
2642
2643        for plot in new_plots:
2644            self.communicate.plotUpdateSignal.emit([plot])
2645
2646        # Update radius_effective if relevant
2647        self.updateEffectiveRadius(return_data)
2648
2649    def complete2D(self, return_data):
2650        """
2651        Plot the current 2D data
2652        """
2653        # Bring the GUI to normal state
2654        self.enableInteractiveElements()
2655
2656        if return_data is None:
2657            return
2658
2659        fitted_data = self.logic.new2DPlot(return_data)
2660        # assure the current index is set properly for batch
2661        if len(self._logic) > 1:
2662            for i, logic in enumerate(self._logic):
2663                if logic.data.name in fitted_data.name:
2664                    self.data_index = i
2665
2666        residuals = self.calculateResiduals(fitted_data)
2667        self.model_data = fitted_data
2668        new_plots = [fitted_data]
2669        if residuals is not None:
2670            new_plots.append(residuals)
2671
2672        # Update/generate plots
2673        for plot in new_plots:
2674            self.communicate.plotUpdateSignal.emit([plot])
2675
2676    def updateEffectiveRadius(self, return_data):
2677        """
2678        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2679        calculated value as returned by sasmodels (if the value was returned).
2680        """
2681        ER_mode_row = self.getRowFromName("radius_effective_mode")
2682        if ER_mode_row is None:
2683            return
2684        try:
2685            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2686        except ValueError:
2687            logging.error("radius_effective_mode was set to an invalid value.")
2688            return
2689        if ER_mode < 1:
2690            # does not need updating if it is not being computed
2691            return
2692
2693        ER_row = self.getRowFromName("radius_effective")
2694        if ER_row is None:
2695            return
2696
2697        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2698        ER_value = scalar_results.get("effective_radius") # note name of key
2699        if ER_value is None:
2700            return
2701        # ensure the model does not recompute when updating the value
2702        self._model_model.blockSignals(True)
2703        self._model_model.item(ER_row, 1).setText(str(ER_value))
2704        self._model_model.blockSignals(False)
2705        # ensure the view is updated immediately
2706        self._model_model.layoutChanged.emit()
2707
2708    def calculateResiduals(self, fitted_data):
2709        """
2710        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2711        """
2712        # Create a new index for holding data
2713        fitted_data.symbol = "Line"
2714
2715        # Modify fitted_data with weighting
2716        weighted_data = self.addWeightingToData(fitted_data)
2717
2718        self.createNewIndex(weighted_data)
2719
2720        # Calculate difference between return_data and logic.data
2721        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2722        # Update the control
2723        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2724        self.lblChi2Value.setText(chi2_repr)
2725
2726        # Plot residuals if actual data
2727        if not self.data_is_loaded:
2728            return
2729
2730        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2731        if residuals_plot is None:
2732            return
2733        residuals_plot.id = "Residual " + residuals_plot.id
2734        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2735        self.createNewIndex(residuals_plot)
2736        return residuals_plot
2737
2738    def onCategoriesChanged(self):
2739            """
2740            Reload the category/model comboboxes
2741            """
2742            # Store the current combo indices
2743            current_cat = self.cbCategory.currentText()
2744            current_model = self.cbModel.currentText()
2745
2746            # reread the category file and repopulate the combo
2747            self.cbCategory.blockSignals(True)
2748            self.cbCategory.clear()
2749            self.readCategoryInfo()
2750            self.initializeCategoryCombo()
2751
2752            # Scroll back to the original index in Categories
2753            new_index = self.cbCategory.findText(current_cat)
2754            if new_index != -1:
2755                self.cbCategory.setCurrentIndex(new_index)
2756            self.cbCategory.blockSignals(False)
2757            # ...and in the Models
2758            self.cbModel.blockSignals(True)
2759            new_index = self.cbModel.findText(current_model)
2760            if new_index != -1:
2761                self.cbModel.setCurrentIndex(new_index)
2762            self.cbModel.blockSignals(False)
2763
2764            return
2765
2766    def calcException(self, etype, value, tb):
2767        """
2768        Thread threw an exception.
2769        """
2770        # Bring the GUI to normal state
2771        self.enableInteractiveElements()
2772        # TODO: remimplement thread cancellation
2773        logger.error("".join(traceback.format_exception(etype, value, tb)))
2774
2775    def setTableProperties(self, table):
2776        """
2777        Setting table properties
2778        """
2779        # Table properties
2780        table.verticalHeader().setVisible(False)
2781        table.setAlternatingRowColors(True)
2782        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2783        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2784        table.resizeColumnsToContents()
2785
2786        # Header
2787        header = table.horizontalHeader()
2788        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2789        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2790
2791        # Qt5: the following 2 lines crash - figure out why!
2792        # Resize column 0 and 7 to content
2793        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2794        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2795
2796    def setPolyModel(self):
2797        """
2798        Set polydispersity values
2799        """
2800        if not self.model_parameters:
2801            return
2802        self._poly_model.clear()
2803
2804        parameters = self.model_parameters.form_volume_parameters
2805        if self.is2D:
2806            parameters += self.model_parameters.orientation_parameters
2807
2808        [self.setPolyModelParameters(i, param) for i, param in \
2809            enumerate(parameters) if param.polydisperse]
2810
2811        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2812
2813    def setPolyModelParameters(self, i, param):
2814        """
2815        Standard of multishell poly parameter driver
2816        """
2817        param_name = param.name
2818        # see it the parameter is multishell
2819        if '[' in param.name:
2820            # Skip empty shells
2821            if self.current_shell_displayed == 0:
2822                return
2823            else:
2824                # Create as many entries as current shells
2825                for ishell in range(1, self.current_shell_displayed+1):
2826                    # Remove [n] and add the shell numeral
2827                    name = param_name[0:param_name.index('[')] + str(ishell)
2828                    self.addNameToPolyModel(i, name)
2829        else:
2830            # Just create a simple param entry
2831            self.addNameToPolyModel(i, param_name)
2832
2833    def addNameToPolyModel(self, i, param_name):
2834        """
2835        Creates a checked row in the poly model with param_name
2836        """
2837        # Polydisp. values from the sasmodel
2838        width = self.kernel_module.getParam(param_name + '.width')
2839        npts = self.kernel_module.getParam(param_name + '.npts')
2840        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2841        _, min, max = self.kernel_module.details[param_name]
2842
2843        # Update local param dict
2844        self.poly_params[param_name + '.width'] = width
2845        self.poly_params[param_name + '.npts'] = npts
2846        self.poly_params[param_name + '.nsigmas'] = nsigs
2847
2848        # Construct a row with polydisp. related variable.
2849        # This will get added to the polydisp. model
2850        # Note: last argument needs extra space padding for decent display of the control
2851        checked_list = ["Distribution of " + param_name, str(width),
2852                        str(min), str(max),
2853                        str(npts), str(nsigs), "gaussian      ",'']
2854        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2855
2856        # All possible polydisp. functions as strings in combobox
2857        func = QtWidgets.QComboBox()
2858        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2859        # Set the default index
2860        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2861        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2862        self.lstPoly.setIndexWidget(ind, func)
2863        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2864
2865    def onPolyFilenameChange(self, row_index):
2866        """
2867        Respond to filename_updated signal from the delegate
2868        """
2869        # For the given row, invoke the "array" combo handler
2870        array_caption = 'array'
2871
2872        # Get the combo box reference
2873        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2874        widget = self.lstPoly.indexWidget(ind)
2875
2876        # Update the combo box so it displays "array"
2877        widget.blockSignals(True)
2878        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2879        widget.blockSignals(False)
2880
2881        # Invoke the file reader
2882        self.onPolyComboIndexChange(array_caption, row_index)
2883
2884    def onPolyComboIndexChange(self, combo_string, row_index):
2885        """
2886        Modify polydisp. defaults on function choice
2887        """
2888        # Get npts/nsigs for current selection
2889        param = self.model_parameters.form_volume_parameters[row_index]
2890        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2891        combo_box = self.lstPoly.indexWidget(file_index)
2892
2893        def updateFunctionCaption(row):
2894            # Utility function for update of polydispersity function name in the main model
2895            if not self.isCheckable(row):
2896                return
2897            self._model_model.blockSignals(True)
2898            param_name = str(self._model_model.item(row, 0).text())
2899            self._model_model.blockSignals(False)
2900            if param_name !=  param.name:
2901                return
2902            # Modify the param value
2903            self._model_model.blockSignals(True)
2904            if self.has_error_column:
2905                # err column changes the indexing
2906                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2907            else:
2908                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2909            self._model_model.blockSignals(False)
2910
2911        if combo_string == 'array':
2912            try:
2913                self.loadPolydispArray(row_index)
2914                # Update main model for display
2915                self.iterateOverModel(updateFunctionCaption)
2916                # disable the row
2917                lo = self.lstPoly.itemDelegate().poly_pd
2918                hi = self.lstPoly.itemDelegate().poly_function
2919                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2920                return
2921            except IOError:
2922                combo_box.setCurrentIndex(self.orig_poly_index)
2923                # Pass for cancel/bad read
2924                pass
2925
2926        # Enable the row in case it was disabled by Array
2927        self._poly_model.blockSignals(True)
2928        max_range = self.lstPoly.itemDelegate().poly_filename
2929        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2930        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2931        self._poly_model.setData(file_index, "")
2932        self._poly_model.blockSignals(False)
2933
2934        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2935        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2936
2937        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2938        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2939
2940        self._poly_model.setData(npts_index, npts)
2941        self._poly_model.setData(nsigs_index, nsigs)
2942
2943        self.iterateOverModel(updateFunctionCaption)
2944        self.orig_poly_index = combo_box.currentIndex()
2945
2946    def loadPolydispArray(self, row_index):
2947        """
2948        Show the load file dialog and loads requested data into state
2949        """
2950        datafile = QtWidgets.QFileDialog.getOpenFileName(
2951            self, "Choose a weight file", "", "All files (*.*)", None,
2952            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2953
2954        if not datafile:
2955            logger.info("No weight data chosen.")
2956            raise IOError
2957
2958        values = []
2959        weights = []
2960        def appendData(data_tuple):
2961            """
2962            Fish out floats from a tuple of strings
2963            """
2964            try:
2965                values.append(float(data_tuple[0]))
2966                weights.append(float(data_tuple[1]))
2967            except (ValueError, IndexError):
2968                # just pass through if line with bad data
2969                return
2970
2971        with open(datafile, 'r') as column_file:
2972            column_data = [line.rstrip().split() for line in column_file.readlines()]
2973            [appendData(line) for line in column_data]
2974
2975        # If everything went well - update the sasmodel values
2976        self.disp_model = POLYDISPERSITY_MODELS['array']()
2977        self.disp_model.set_weights(np.array(values), np.array(weights))
2978        # + update the cell with filename
2979        fname = os.path.basename(str(datafile))
2980        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2981        self._poly_model.setData(fname_index, fname)
2982
2983    def onColumnWidthUpdate(self, index, old_size, new_size):
2984        """
2985        Simple state update of the current column widths in the  param list
2986        """
2987        self.lstParamHeaderSizes[index] = new_size
2988
2989    def setMagneticModel(self):
2990        """
2991        Set magnetism values on model
2992        """
2993        if not self.model_parameters:
2994            return
2995        self._magnet_model.clear()
2996        # default initial value
2997        m0 = 0.5
2998        for param in self.model_parameters.call_parameters:
2999            if param.type != 'magnetic': continue
3000            if "M0" in param.name:
3001                m0 += 0.5
3002                value = m0
3003            else:
3004                value = param.default
3005            self.addCheckedMagneticListToModel(param, value)
3006
3007        FittingUtilities.addHeadersToModel(self._magnet_model)
3008
3009    def shellNamesList(self):
3010        """
3011        Returns list of names of all multi-shell parameters
3012        E.g. for sld[n], radius[n], n=1..3 it will return
3013        [sld1, sld2, sld3, radius1, radius2, radius3]
3014        """
3015        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3016        top_index = self.kernel_module.multiplicity_info.number
3017        shell_names = []
3018        for i in range(1, top_index+1):
3019            for name in multi_names:
3020                shell_names.append(name+str(i))
3021        return shell_names
3022
3023    def addCheckedMagneticListToModel(self, param, value):
3024        """
3025        Wrapper for model update with a subset of magnetic parameters
3026        """
3027        try:
3028            basename, _ = param.name.rsplit('_', 1)
3029        except ValueError:
3030            basename = param.name
3031        if basename in self.shell_names:
3032            try:
3033                shell_index = int(basename[-2:])
3034            except ValueError:
3035                shell_index = int(basename[-1:])
3036
3037            if shell_index > self.current_shell_displayed:
3038                return
3039
3040        checked_list = [param.name,
3041                        str(value),
3042                        str(param.limits[0]),
3043                        str(param.limits[1]),
3044                        param.units]
3045
3046        self.magnet_params[param.name] = value
3047
3048        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
3049
3050    def enableStructureFactorControl(self, structure_factor):
3051        """
3052        Add structure factors to the list of parameters
3053        """
3054        if self.kernel_module.is_form_factor or structure_factor == 'None':
3055            self.enableStructureCombo()
3056        else:
3057            self.disableStructureCombo()
3058
3059    def addExtraShells(self):
3060        """
3061        Add a combobox for multiple shell display
3062        """
3063        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3064
3065        if param_length == 0:
3066            return
3067
3068        # cell 1: variable name
3069        item1 = QtGui.QStandardItem(param_name)
3070
3071        func = QtWidgets.QComboBox()
3072
3073        # cell 2: combobox
3074        item2 = QtGui.QStandardItem()
3075
3076        # cell 3: min value
3077        item3 = QtGui.QStandardItem()
3078
3079        # cell 4: max value
3080        item4 = QtGui.QStandardItem()
3081
3082        # cell 4: SLD button
3083        item5 = QtGui.QStandardItem()
3084        button = QtWidgets.QPushButton()
3085        button.setText("Show SLD Profile")
3086
3087        self._model_model.appendRow([item1, item2, item3, item4, item5])
3088
3089        # Beautify the row:  span columns 2-4
3090        shell_row = self._model_model.rowCount()
3091        shell_index = self._model_model.index(shell_row-1, 1)
3092        button_index = self._model_model.index(shell_row-1, 4)
3093
3094        self.lstParams.setIndexWidget(shell_index, func)
3095        self.lstParams.setIndexWidget(button_index, button)
3096        self._n_shells_row = shell_row - 1
3097
3098        # Get the default number of shells for the model
3099        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3100        shell_par = None
3101        for par in kernel_pars:
3102            if par.name == param_name:
3103                shell_par = par
3104                break
3105        if not shell_par:
3106            logger.error("Could not find %s in kernel parameters.", param_name)
3107        default_shell_count = shell_par.default
3108        shell_min = 0
3109        shell_max = 0
3110        try:
3111            shell_min = int(shell_par.limits[0])
3112            shell_max = int(shell_par.limits[1])
3113        except IndexError as ex:
3114            # no info about limits
3115            pass
3116        # don't update the kernel here - this data is display only
3117        self._model_model.blockSignals(True)
3118        item3.setText(str(shell_min))
3119        item4.setText(str(shell_max))
3120        self._model_model.blockSignals(False)
3121
3122        # Respond to index change
3123        func.currentTextChanged.connect(self.modifyShellsInList)
3124
3125        # Respond to button press
3126        button.clicked.connect(self.onShowSLDProfile)
3127
3128        # Available range of shells displayed in the combobox
3129        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3130
3131        # Add default number of shells to the model
3132        func.setCurrentText(str(default_shell_count))
3133
3134    def modifyShellsInList(self, text):
3135        """
3136        Add/remove additional multishell parameters
3137        """
3138        # Find row location of the combobox
3139        first_row = self._n_shells_row + 1
3140        remove_rows = self._num_shell_params
3141        try:
3142            index = int(text)
3143        except ValueError:
3144            # bad text on the control!
3145            index = 0
3146            logger.error("Multiplicity incorrect! Setting to 0")
3147        self.kernel_module.multiplicity = index
3148        if remove_rows > 1:
3149            self._model_model.removeRows(first_row, remove_rows)
3150
3151        new_rows = FittingUtilities.addShellsToModel(
3152                self.model_parameters,
3153                self._model_model,
3154                index,
3155                first_row,
3156                self.lstParams)
3157
3158        self._num_shell_params = len(new_rows)
3159        self.current_shell_displayed = index
3160
3161        # Param values for existing shells were reset to default; force all changes into kernel module
3162        for row in new_rows:
3163            par = row[0].text()
3164            val = GuiUtils.toDouble(row[1].text())
3165            self.kernel_module.setParam(par, val)
3166
3167        # Change 'n' in the parameter model; also causes recalculation
3168        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3169
3170        # Update relevant models
3171        self.setPolyModel()
3172        if self.canHaveMagnetism():
3173            self.setMagneticModel()
3174
3175    def onShowSLDProfile(self):
3176        """
3177        Show a quick plot of SLD profile
3178        """
3179        # get profile data
3180        x, y = self.kernel_module.getProfile()
3181        y *= 1.0e6
3182        profile_data = Data1D(x=x, y=y)
3183        profile_data.name = "SLD"
3184        profile_data.scale = 'linear'
3185        profile_data.symbol = 'Line'
3186        profile_data.hide_error = True
3187        profile_data._xaxis = "R(\AA)"
3188        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3189
3190        plotter = PlotterWidget(self, quickplot=True)
3191        plotter.data = profile_data
3192        plotter.showLegend = True
3193        plotter.plot(hide_error=True, marker='-')
3194
3195        self.plot_widget = QtWidgets.QWidget()
3196        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3197        layout = QtWidgets.QVBoxLayout()
3198        layout.addWidget(plotter)
3199        self.plot_widget.setLayout(layout)
3200        self.plot_widget.show()
3201
3202    def setInteractiveElements(self, enabled=True):
3203        """
3204        Switch interactive GUI elements on/off
3205        """
3206        assert isinstance(enabled, bool)
3207
3208        self.lstParams.setEnabled(enabled)
3209        self.lstPoly.setEnabled(enabled)
3210        self.lstMagnetic.setEnabled(enabled)
3211
3212        self.cbCategory.setEnabled(enabled)
3213        self.cbModel.setEnabled(enabled)
3214        self.cmdPlot.setEnabled(enabled)
3215
3216    def enableInteractiveElements(self):
3217        """
3218        Set buttion caption on fitting/calculate finish
3219        Enable the param table(s)
3220        """
3221        # Notify the user that fitting is available
3222        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3223        self.cmdFit.setText("Fit")
3224        self.fit_started = False
3225        self.setInteractiveElements(True)
3226
3227    def disableInteractiveElements(self):
3228        """
3229        Set buttion caption on fitting/calculate start
3230        Disable the param table(s)
3231        """
3232        # Notify the user that fitting is being run
3233        # Allow for stopping the job
3234        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3235        self.cmdFit.setText('Stop fit')
3236        self.setInteractiveElements(False)
3237
3238    def readFitPage(self, fp):
3239        """
3240        Read in state from a fitpage object and update GUI
3241        """
3242        assert isinstance(fp, FitPage)
3243        # Main tab info
3244        self.logic.data.filename = fp.filename
3245        self.data_is_loaded = fp.data_is_loaded
3246        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3247        self.chkMagnetism.setCheckState(fp.is_magnetic)
3248        self.chk2DView.setCheckState(fp.is2D)
3249
3250        # Update the comboboxes
3251        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3252        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3253        if fp.current_factor:
3254            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3255
3256        self.chi2 = fp.chi2
3257
3258        # Options tab
3259        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3260        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3261        self.npts = fp.fit_options[fp.NPTS]
3262        self.log_points = fp.fit_options[fp.LOG_POINTS]
3263        self.weighting = fp.fit_options[fp.WEIGHTING]
3264
3265        # Models
3266        self._model_model = fp.model_model
3267        self._poly_model = fp.poly_model
3268        self._magnet_model = fp.magnetism_model
3269
3270        # Resolution tab
3271        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3272        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3273        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3274        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3275        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3276
3277        # TODO: add polidyspersity and magnetism
3278
3279    def saveToFitPage(self, fp):
3280        """
3281        Write current state to the given fitpage
3282        """
3283        assert isinstance(fp, FitPage)
3284
3285        # Main tab info
3286        fp.filename = self.logic.data.filename
3287        fp.data_is_loaded = self.data_is_loaded
3288        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3289        fp.is_magnetic = self.chkMagnetism.isChecked()
3290        fp.is2D = self.chk2DView.isChecked()
3291        fp.data = self.data
3292
3293        # Use current models - they contain all the required parameters
3294        fp.model_model = self._model_model
3295        fp.poly_model = self._poly_model
3296        fp.magnetism_model = self._magnet_model
3297
3298        if self.cbCategory.currentIndex() != 0:
3299            fp.current_category = str(self.cbCategory.currentText())
3300            fp.current_model = str(self.cbModel.currentText())
3301
3302        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3303            fp.current_factor = str(self.cbStructureFactor.currentText())
3304        else:
3305            fp.current_factor = ''
3306
3307        fp.chi2 = self.chi2
3308        fp.main_params_to_fit = self.main_params_to_fit
3309        fp.poly_params_to_fit = self.poly_params_to_fit
3310        fp.magnet_params_to_fit = self.magnet_params_to_fit
3311        fp.kernel_module = self.kernel_module
3312
3313        # Algorithm options
3314        # fp.algorithm = self.parent.fit_options.selected_id
3315
3316        # Options tab
3317        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3318        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3319        fp.fit_options[fp.NPTS] = self.npts
3320        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3321        fp.fit_options[fp.LOG_POINTS] = self.log_points
3322        fp.fit_options[fp.WEIGHTING] = self.weighting
3323
3324        # Resolution tab
3325        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3326        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3327        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3328        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3329        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3330
3331        # TODO: add polidyspersity and magnetism
3332
3333    def updateUndo(self):
3334        """
3335        Create a new state page and add it to the stack
3336        """
3337        if self.undo_supported:
3338            self.pushFitPage(self.currentState())
3339
3340    def currentState(self):
3341        """
3342        Return fit page with current state
3343        """
3344        new_page = FitPage()
3345        self.saveToFitPage(new_page)
3346
3347        return new_page
3348
3349    def pushFitPage(self, new_page):
3350        """
3351        Add a new fit page object with current state
3352        """
3353        self.page_stack.append(new_page)
3354
3355    def popFitPage(self):
3356        """
3357        Remove top fit page from stack
3358        """
3359        if self.page_stack:
3360            self.page_stack.pop()
3361
3362    def getReport(self):
3363        """
3364        Create and return HTML report with parameters and charts
3365        """
3366        index = None
3367        if self.all_data:
3368            index = self.all_data[self.data_index]
3369        else:
3370            index = self.theory_item
3371        params = FittingUtilities.getStandardParam(self._model_model)
3372        report_logic = ReportPageLogic(self,
3373                                       kernel_module=self.kernel_module,
3374                                       data=self.data,
3375                                       index=index,
3376                                       params=params)
3377
3378        return report_logic.reportList()
3379
3380    def savePageState(self):
3381        """
3382        Create and serialize local PageState
3383        """
3384        filepath = self.saveAsAnalysisFile()
3385        if filepath is None or filepath == "":
3386            return
3387
3388        fitpage_state = self.getFitPage()
3389        fitpage_state += self.getFitModel()
3390
3391        with open(filepath, 'w') as statefile:
3392            for line in fitpage_state:
3393                statefile.write(str(line))
3394
3395        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
3396
3397    def saveAsAnalysisFile(self):
3398        """
3399        Show the save as... dialog and return the chosen filepath
3400        """
3401        default_name = "FitPage"+str(self.tab_id)+".fitv"
3402
3403        wildcard = "fitv files (*.fitv)"
3404        kwargs = {
3405            'caption'   : 'Save As',
3406            'directory' : default_name,
3407            'filter'    : wildcard,
3408            'parent'    : None,
3409        }
3410        # Query user for filename.
3411        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3412        filename = filename_tuple[0]
3413        return filename
3414
3415    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3416        """
3417        This is a callback method called from the CANSAS reader.
3418        We need the instance of this reader only for writing out a file,
3419        so there's nothing here.
3420        Until Load Analysis is implemented, that is.
3421        """
3422        pass
3423
3424    def loadPageState(self, pagestate=None):
3425        """
3426        Load the PageState object and update the current widget
3427        """
3428        filepath = self.loadAnalysisFile()
3429        if filepath is None or filepath == "":
3430            return
3431
3432        with open(filepath, 'r') as statefile:
3433            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3434            lines = statefile.readlines()
3435
3436        # convert into list of lists
3437        pass
3438
3439    def loadAnalysisFile(self):
3440        """
3441        Called when the "Open Project" menu item chosen.
3442        """
3443        default_name = "FitPage"+str(self.tab_id)+".fitv"
3444        wildcard = "fitv files (*.fitv)"
3445        kwargs = {
3446            'caption'   : 'Open Analysis',
3447            'directory' : default_name,
3448            'filter'    : wildcard,
3449            'parent'    : self,
3450        }
3451        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3452        return filename
3453
3454    def onCopyToClipboard(self, format=None):
3455        """
3456        Copy current fitting parameters into the clipboard
3457        using requested formatting:
3458        plain, excel, latex
3459        """
3460        param_list = self.getFitParameters()
3461        if format=="":
3462            param_list = self.getFitPage()
3463            param_list += self.getFitModel()
3464            formatted_output = FittingUtilities.formatParameters(param_list)
3465        elif format == "Excel":
3466            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3467        elif format == "Latex":
3468            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3469        else:
3470            raise AttributeError("Bad parameter output format specifier.")
3471
3472        # Dump formatted_output to the clipboard
3473        cb = QtWidgets.QApplication.clipboard()
3474        cb.setText(formatted_output)
3475
3476    def getFitModel(self):
3477        """
3478        serializes combobox state
3479        """
3480        param_list = []
3481        model = str(self.cbModel.currentText())
3482        category = str(self.cbCategory.currentText())
3483        structure = str(self.cbStructureFactor.currentText())
3484        param_list.append(['fitpage_category', category])
3485        param_list.append(['fitpage_model', model])
3486        param_list.append(['fitpage_structure', structure])
3487
3488        return param_list
3489
3490    def getFitPage(self):
3491        """
3492        serializes full state of this fit page
3493        """
3494        # run a loop over all parameters and pull out
3495        # first - regular params
3496        param_list = self.getFitParameters()
3497
3498        param_list.append(['is_data', str(self.data_is_loaded)])
3499        if self.data_is_loaded:
3500            param_list.append(['data_id', str(self.logic.data.id)])
3501            param_list.append(['data_name', str(self.logic.data.filename)])
3502
3503        # option tab
3504        param_list.append(['q_range_min', str(self.q_range_min)])
3505        param_list.append(['q_range_max', str(self.q_range_max)])
3506        param_list.append(['q_weighting', str(self.weighting)])
3507        param_list.append(['weighting', str(self.options_widget.weighting)])
3508
3509        # resolution
3510        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3511        index = self.smearing_widget.cbSmearing.currentIndex()
3512        param_list.append(['smearing', str(index)])
3513        param_list.append(['smearing_min', str(smearing_min)])
3514        param_list.append(['smearing_max', str(smearing_max)])
3515
3516        # checkboxes, if required
3517        has_polydisp = self.chkPolydispersity.isChecked()
3518        has_magnetism = self.chkMagnetism.isChecked()
3519        has_chain = self.chkChainFit.isChecked()
3520        has_2D = self.chk2DView.isChecked()
3521        param_list.append(['polydisperse_params', str(has_polydisp)])
3522        param_list.append(['magnetic_params', str(has_magnetism)])
3523        param_list.append(['chainfit_params', str(has_chain)])
3524        param_list.append(['2D_params', str(has_2D)])
3525
3526        return param_list
3527
3528    def getFitParameters(self):
3529        """
3530        serializes current parameters
3531        """
3532        param_list = []
3533        param_list.append(['model_name', str(self.cbModel.currentText())])
3534
3535        def gatherParams(row):
3536            """
3537            Create list of main parameters based on _model_model
3538            """
3539            param_name = str(self._model_model.item(row, 0).text())
3540
3541            # Assure this is a parameter - must contain a checkbox
3542            if not self._model_model.item(row, 0).isCheckable():
3543                # maybe it is a combobox item (multiplicity)
3544                try:
3545                    index = self._model_model.index(row, 1)
3546                    widget = self.lstParams.indexWidget(index)
3547                    if widget is None:
3548                        return
3549                    if isinstance(widget, QtWidgets.QComboBox):
3550                        # find the index of the combobox
3551                        current_index = widget.currentIndex()
3552                        param_list.append([param_name, 'None', str(current_index)])
3553                except Exception as ex:
3554                    pass
3555                return
3556
3557            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3558            # Value of the parameter. In some cases this is the text of the combobox choice.
3559            param_value = str(self._model_model.item(row, 1).text())
3560            param_error = None
3561            param_min = None
3562            param_max = None
3563            column_offset = 0
3564            if self.has_error_column:
3565                column_offset = 1
3566                param_error = str(self._model_model.item(row, 1+column_offset).text())
3567            try:
3568                param_min = str(self._model_model.item(row, 2+column_offset).text())
3569                param_max = str(self._model_model.item(row, 3+column_offset).text())
3570            except:
3571                pass
3572
3573            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3574
3575        def gatherPolyParams(row):
3576            """
3577            Create list of polydisperse parameters based on _poly_model
3578            """
3579            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3580            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3581            param_value = str(self._poly_model.item(row, 1).text())
3582            param_error = None
3583            column_offset = 0
3584            if self.has_poly_error_column:
3585                column_offset = 1
3586                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3587            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3588            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3589            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3590            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3591            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3592            index = self._poly_model.index(row, 6+column_offset)
3593            widget = self.lstPoly.indexWidget(index)
3594            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3595                param_fun = widget.currentText()
3596            # width
3597            name = param_name+".width"
3598            param_list.append([name, param_checked, param_value, param_error,
3599                               param_min, param_max, param_npts, param_nsigs, param_fun])
3600
3601        def gatherMagnetParams(row):
3602            """
3603            Create list of magnetic parameters based on _magnet_model
3604            """
3605            param_name = str(self._magnet_model.item(row, 0).text())
3606            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3607            param_value = str(self._magnet_model.item(row, 1).text())
3608            param_error = None
3609            column_offset = 0
3610            if self.has_magnet_error_column:
3611                column_offset = 1
3612                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3613            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3614            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3615            param_list.append([param_name, param_checked, param_value,
3616                               param_error, param_min, param_max])
3617
3618        self.iterateOverModel(gatherParams)
3619        if self.chkPolydispersity.isChecked():
3620            self.iterateOverPolyModel(gatherPolyParams)
3621        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3622            self.iterateOverMagnetModel(gatherMagnetParams)
3623
3624        if self.kernel_module.is_multiplicity_model:
3625            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3626
3627        return param_list
3628
3629    def onParameterPaste(self):
3630        """
3631        Use the clipboard to update fit state
3632        """
3633        # Check if the clipboard contains right stuff
3634        cb = QtWidgets.QApplication.clipboard()
3635        cb_text = cb.text()
3636
3637        context = {}
3638        lines = cb_text.split(':')
3639        if lines[0] != 'sasview_parameter_values':
3640            return False
3641
3642        # put the text into dictionary
3643        line_dict = {}
3644        for line in lines[1:]:
3645            content = line.split(',')
3646            if len(content) > 1:
3647                line_dict[content[0]] = content[1:]
3648
3649        model = line_dict['model_name'][0]
3650
3651        if 'model_name' not in line_dict.keys():
3652            return False
3653
3654        if 'multiplicity' in line_dict.keys():
3655            multip = int(line_dict['multiplicity'][0], 0)
3656            # reset the model with multiplicity, so further updates are saved
3657            if self.kernel_module.is_multiplicity_model:
3658                self.kernel_module.multiplicity=multip
3659                self.updateMultiplicityCombo(multip)
3660
3661        if 'polydisperse_params' in line_dict.keys():
3662            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3663        if 'magnetic_params' in line_dict.keys():
3664            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3665        if 'chainfit_params' in line_dict.keys():
3666            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3667        if '2D_params' in line_dict.keys():
3668            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3669
3670        # Create the context dictionary for parameters
3671        context['model_name'] = model
3672        for key, value in line_dict.items():
3673            if len(value) > 2:
3674                context[key] = value
3675
3676        if str(self.cbModel.currentText()) != str(context['model_name']):
3677            msg = QtWidgets.QMessageBox()
3678            msg.setIcon(QtWidgets.QMessageBox.Information)
3679            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3680                         Not all parameters saved may paste correctly.")
3681            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3682            result = msg.exec_()
3683            if result == QtWidgets.QMessageBox.Ok:
3684                pass
3685            else:
3686                return
3687
3688        if 'smearing' in line_dict.keys():
3689            try:
3690                index = int(line_dict['smearing'][0])
3691                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3692            except ValueError:
3693                pass
3694        if 'smearing_min' in line_dict.keys():
3695            try:
3696                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3697            except ValueError:
3698                pass
3699        if 'smearing_max' in line_dict.keys():
3700            try:
3701                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3702            except ValueError:
3703                pass
3704
3705        if 'q_range_max' in line_dict.keys():
3706            try:
3707                self.q_range_min = float(line_dict['q_range_min'][0])
3708                self.q_range_max = float(line_dict['q_range_max'][0])
3709            except ValueError:
3710                pass
3711        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3712        try:
3713            button_id = int(line_dict['weighting'][0])
3714            for button in self.options_widget.weightingGroup.buttons():
3715                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3716                    button.setChecked(True)
3717                    break
3718        except ValueError:
3719            pass
3720
3721        self.updateFullModel(context)
3722        self.updateFullPolyModel(context)
3723        self.updateFullMagnetModel(context)
3724
3725    def updateMultiplicityCombo(self, multip):
3726        """
3727        Find and update the multiplicity combobox
3728        """
3729        index = self._model_model.index(self._n_shells_row, 1)
3730        widget = self.lstParams.indexWidget(index)
3731        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3732            widget.setCurrentIndex(widget.findText(str(multip)))
3733        self.current_shell_displayed = multip
3734
3735    def updateFullModel(self, param_dict):
3736        """
3737        Update the model with new parameters
3738        """
3739        assert isinstance(param_dict, dict)
3740        if not dict:
3741            return
3742
3743        def updateFittedValues(row):
3744            # Utility function for main model update
3745            # internal so can use closure for param_dict
3746            param_name = str(self._model_model.item(row, 0).text())
3747            if param_name not in list(param_dict.keys()):
3748                return
3749            # Special case of combo box in the cell (multiplicity)
3750            param_line = param_dict[param_name]
3751            if len(param_line) == 1:
3752                # modify the shells value
3753                try:
3754                    combo_index = int(param_line[0])
3755                except ValueError:
3756                    # quietly pass
3757                    return
3758                index = self._model_model.index(row, 1)
3759                widget = self.lstParams.indexWidget(index)
3760                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3761                    #widget.setCurrentIndex(combo_index)
3762                    return
3763            # checkbox state
3764            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3765            self._model_model.item(row, 0).setCheckState(param_checked)
3766
3767            # parameter value can be either just a value or text on the combobox
3768            param_text = param_dict[param_name][1]
3769            index = self._model_model.index(row, 1)
3770            widget = self.lstParams.indexWidget(index)
3771            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3772                # Find the right index based on text
3773                combo_index = int(param_text, 0)
3774                widget.setCurrentIndex(combo_index)
3775            else:
3776                # modify the param value
3777                param_repr = GuiUtils.formatNumber(param_text, high=True)
3778                self._model_model.item(row, 1).setText(param_repr)
3779
3780            # Potentially the error column
3781            ioffset = 0
3782            joffset = 0
3783            if len(param_dict[param_name])>4:
3784                # error values are not editable - no need to update
3785                ioffset = 1
3786            if self.has_error_column:
3787                joffset = 1
3788            # min/max
3789            try:
3790                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3791                self._model_model.item(row, 2+joffset).setText(param_repr)
3792                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3793                self._model_model.item(row, 3+joffset).setText(param_repr)
3794            except:
3795                pass
3796
3797            self.setFocus()
3798
3799        self.iterateOverModel(updateFittedValues)
3800
3801    def updateFullPolyModel(self, param_dict):
3802        """
3803        Update the polydispersity model with new parameters, create the errors column
3804        """
3805        assert isinstance(param_dict, dict)
3806        if not dict:
3807            return
3808
3809        def updateFittedValues(row):
3810            # Utility function for main model update
3811            # internal so can use closure for param_dict
3812            if row >= self._poly_model.rowCount():
3813                return
3814            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3815            if param_name not in list(param_dict.keys()):
3816                return
3817            # checkbox state
3818            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3819            self._poly_model.item(row,0).setCheckState(param_checked)
3820
3821            # modify the param value
3822            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3823            self._poly_model.item(row, 1).setText(param_repr)
3824
3825            # Potentially the error column
3826            ioffset = 0
3827            joffset = 0
3828            if len(param_dict[param_name])>7:
3829                ioffset = 1
3830            if self.has_poly_error_column:
3831                joffset = 1
3832            # min
3833            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3834            self._poly_model.item(row, 2+joffset).setText(param_repr)
3835            # max
3836            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3837            self._poly_model.item(row, 3+joffset).setText(param_repr)
3838            # Npts
3839            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3840            self._poly_model.item(row, 4+joffset).setText(param_repr)
3841            # Nsigs
3842            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3843            self._poly_model.item(row, 5+joffset).setText(param_repr)
3844
3845            self.setFocus()
3846
3847        self.iterateOverPolyModel(updateFittedValues)
3848
3849    def updateFullMagnetModel(self, param_dict):
3850        """
3851        Update the magnetism model with new parameters, create the errors column
3852        """
3853        assert isinstance(param_dict, dict)
3854        if not dict:
3855            return
3856
3857        def updateFittedValues(row):
3858            # Utility function for main model update
3859            # internal so can use closure for param_dict
3860            if row >= self._magnet_model.rowCount():
3861                return
3862            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3863            if param_name not in list(param_dict.keys()):
3864                return
3865            # checkbox state
3866            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3867            self._magnet_model.item(row,0).setCheckState(param_checked)
3868
3869            # modify the param value
3870            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3871            self._magnet_model.item(row, 1).setText(param_repr)
3872
3873            # Potentially the error column
3874            ioffset = 0
3875            joffset = 0
3876            if len(param_dict[param_name])>4:
3877                ioffset = 1
3878            if self.has_magnet_error_column:
3879                joffset = 1
3880            # min
3881            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3882            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3883            # max
3884            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3885            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3886
3887        self.iterateOverMagnetModel(updateFittedValues)
3888
3889    def getCurrentFitState(self, state=None):
3890        """
3891        Store current state for fit_page
3892        """
3893        # save model option
3894        #if self.model is not None:
3895        #    self.disp_list = self.getDispParamList()
3896        #    state.disp_list = copy.deepcopy(self.disp_list)
3897        #    #state.model = self.model.clone()
3898
3899        # Comboboxes
3900        state.categorycombobox = self.cbCategory.currentText()
3901        state.formfactorcombobox = self.cbModel.currentText()
3902        if self.cbStructureFactor.isEnabled():
3903            state.structurecombobox = self.cbStructureFactor.currentText()
3904        state.tcChi = self.chi2
3905
3906        state.enable2D = self.is2D
3907
3908        #state.weights = copy.deepcopy(self.weights)
3909        # save data
3910        state.data = copy.deepcopy(self.data)
3911
3912        # save plotting range
3913        state.qmin = self.q_range_min
3914        state.qmax = self.q_range_max
3915        state.npts = self.npts
3916
3917        #    self.state.enable_disp = self.enable_disp.GetValue()
3918        #    self.state.disable_disp = self.disable_disp.GetValue()
3919
3920        #    self.state.enable_smearer = \
3921        #                        copy.deepcopy(self.enable_smearer.GetValue())
3922        #    self.state.disable_smearer = \
3923        #                        copy.deepcopy(self.disable_smearer.GetValue())
3924
3925        #self.state.pinhole_smearer = \
3926        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3927        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3928        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3929        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3930        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3931        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3932
3933        p = self.model_parameters
3934        # save checkbutton state and txtcrtl values
3935        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3936        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3937
3938        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3939        #self._copy_parameters_state(self.parameters, self.state.parameters)
3940        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3941        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3942
Note: See TracBrowser for help on using the repository browser.