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

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

Reworked the complex constraint functionality SASVIEW-1019

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