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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_opencl
Last change on this file since f5e2a10a was f5e2a10a, checked in by ibressler, 5 years ago

show a polydispersity plot for 2D data as well

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