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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_sync_sascalc
Last change on this file since 1d6899f was 1d6899f, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 4 years ago

Removed superfluous assignment. qmin/qmax is already defined and
fetching it from the control seems to create more problems for modified
q range re-fits.

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