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

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

Merge branch 'ESS_GUI' into ESS_GUI_project_save

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