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

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

Be careful with standard item lifetime. SASVIEW-1231

  • Property mode set to 100644
File size: 161.5 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
287        # Which shell is being currently displayed?
288        self.current_shell_displayed = 0
289        # List of all shell-unique parameters
290        self.shell_names = []
291
292        # Error column presence in parameter display
293        self.has_error_column = False
294        self.has_poly_error_column = False
295        self.has_magnet_error_column = False
296
297        # Enablement of comboboxes
298        self.enabled_cbmodel = False
299        self.enabled_sfmodel = False
300
301        # If the widget generated theory item, save it
302        self.theory_item = None
303
304        # list column widths
305        self.lstParamHeaderSizes = {}
306
307        # signal communicator
308        self.communicate = self.parent.communicate
309
310    def initializeWidgets(self):
311        """
312        Initialize widgets for tabs
313        """
314        # Options widget
315        layout = QtWidgets.QGridLayout()
316        self.options_widget = OptionsWidget(self, self.logic)
317        layout.addWidget(self.options_widget)
318        self.tabOptions.setLayout(layout)
319
320        # Smearing widget
321        layout = QtWidgets.QGridLayout()
322        self.smearing_widget = SmearingWidget(self)
323        layout.addWidget(self.smearing_widget)
324        self.tabResolution.setLayout(layout)
325
326        # Define bold font for use in various controls
327        self.boldFont = QtGui.QFont()
328        self.boldFont.setBold(True)
329
330        # Set data label
331        self.label.setFont(self.boldFont)
332        self.label.setText("No data loaded")
333        self.lblFilename.setText("")
334
335        # Magnetic angles explained in one picture
336        self.magneticAnglesWidget = QtWidgets.QWidget()
337        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
338        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.png')
339        labl.setPixmap(pixmap)
340        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
341
342    def initializeModels(self):
343        """
344        Set up models and views
345        """
346        # Set the main models
347        # We can't use a single model here, due to restrictions on flattening
348        # the model tree with subclassed QAbstractProxyModel...
349        self._model_model = ToolTippedItemModel()
350        self._poly_model = ToolTippedItemModel()
351        self._magnet_model = ToolTippedItemModel()
352
353        # Param model displayed in param list
354        self.lstParams.setModel(self._model_model)
355        self.readCategoryInfo()
356
357        self.model_parameters = None
358
359        # Delegates for custom editing and display
360        self.lstParams.setItemDelegate(ModelViewDelegate(self))
361
362        self.lstParams.setAlternatingRowColors(True)
363        stylesheet = """
364
365            QTreeView {
366                paint-alternating-row-colors-for-empty-area:0;
367            }
368
369            QTreeView::item {
370                border: 1px;
371                padding: 2px 1px;
372            }
373
374            QTreeView::item:hover {
375                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
376                border: 1px solid #bfcde4;
377            }
378
379            QTreeView::item:selected {
380                border: 1px solid #567dbc;
381            }
382
383            QTreeView::item:selected:active{
384                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
385            }
386
387            QTreeView::item:selected:!active {
388                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
389            }
390           """
391        self.lstParams.setStyleSheet(stylesheet)
392        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
393        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
394        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
395        # Column resize signals
396        self.lstParams.header().sectionResized.connect(self.onColumnWidthUpdate)
397
398        # Poly model displayed in poly list
399        self.lstPoly.setModel(self._poly_model)
400        self.setPolyModel()
401        self.setTableProperties(self.lstPoly)
402        # Delegates for custom editing and display
403        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
404        # Polydispersity function combo response
405        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
406        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
407
408        # Magnetism model displayed in magnetism list
409        self.lstMagnetic.setModel(self._magnet_model)
410        self.setMagneticModel()
411        self.setTableProperties(self.lstMagnetic)
412        # Delegates for custom editing and display
413        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
414
415    def initializeCategoryCombo(self):
416        """
417        Model category combo setup
418        """
419        category_list = sorted(self.master_category_dict.keys())
420        self.cbCategory.addItem(CATEGORY_DEFAULT)
421        self.cbCategory.addItems(category_list)
422        if CATEGORY_STRUCTURE not in category_list:
423            self.cbCategory.addItem(CATEGORY_STRUCTURE)
424        self.cbCategory.setCurrentIndex(0)
425
426    def setEnablementOnDataLoad(self):
427        """
428        Enable/disable various UI elements based on data loaded
429        """
430        # Tag along functionality
431        self.label.setText("Data loaded from: ")
432        if self.logic.data.filename:
433            self.lblFilename.setText(self.logic.data.filename)
434        else:
435            self.lblFilename.setText(self.logic.data.name)
436        self.updateQRange()
437        # Switch off Data2D control
438        self.chk2DView.setEnabled(False)
439        self.chk2DView.setVisible(False)
440        self.chkMagnetism.setEnabled(False)
441        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked())
442        # Combo box or label for file name"
443        if self.is_batch_fitting:
444            self.lblFilename.setVisible(False)
445            for dataitem in self.all_data:
446                filename = GuiUtils.dataFromItem(dataitem).filename
447                self.cbFileNames.addItem(filename)
448            self.cbFileNames.setVisible(True)
449            self.chkChainFit.setEnabled(True)
450            self.chkChainFit.setVisible(True)
451            # This panel is not designed to view individual fits, so disable plotting
452            self.cmdPlot.setVisible(False)
453        # Similarly on other tabs
454        self.options_widget.setEnablementOnDataLoad()
455        self.onSelectModel()
456        # Smearing tab
457        self.smearing_widget.updateData(self.data)
458
459    def acceptsData(self):
460        """ Tells the caller this widget can accept new dataset """
461        return not self.data_is_loaded
462
463    def disableModelCombo(self):
464        """ Disable the combobox """
465        self.cbModel.setEnabled(False)
466        self.lblModel.setEnabled(False)
467        self.enabled_cbmodel = False
468
469    def enableModelCombo(self):
470        """ Enable the combobox """
471        self.cbModel.setEnabled(True)
472        self.lblModel.setEnabled(True)
473        self.enabled_cbmodel = True
474
475    def disableStructureCombo(self):
476        """ Disable the combobox """
477        self.cbStructureFactor.setEnabled(False)
478        self.lblStructure.setEnabled(False)
479        self.enabled_sfmodel = False
480
481    def enableStructureCombo(self):
482        """ Enable the combobox """
483        self.cbStructureFactor.setEnabled(True)
484        self.lblStructure.setEnabled(True)
485        self.enabled_sfmodel = True
486
487    def togglePoly(self, isChecked):
488        """ Enable/disable the polydispersity tab """
489        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
490        # Check if any parameters are ready for fitting
491        self.cmdFit.setEnabled(self.haveParamsToFit())
492
493    def toggleMagnetism(self, isChecked):
494        """ Enable/disable the magnetism tab """
495        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
496        # Check if any parameters are ready for fitting
497        self.cmdFit.setEnabled(self.haveParamsToFit())
498
499    def toggleChainFit(self, isChecked):
500        """ Enable/disable chain fitting """
501        self.is_chain_fitting = isChecked
502
503    def toggle2D(self, isChecked):
504        """ Enable/disable the controls dependent on 1D/2D data instance """
505        self.chkMagnetism.setEnabled(isChecked)
506        self.is2D = isChecked
507        # Reload the current model
508        if self.kernel_module:
509            self.onSelectModel()
510
511    @classmethod
512    def customModels(cls):
513        """ Reads in file names in the custom plugin directory """
514        return ModelUtilities._find_models()
515
516    def initializeControls(self):
517        """
518        Set initial control enablement
519        """
520        self.cbFileNames.setVisible(False)
521        self.cmdFit.setEnabled(False)
522        self.cmdPlot.setEnabled(False)
523        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
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            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1770            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1771                            qmax=qmax)
1772            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1773            if fitter is None:
1774                # Assign id to the new fitter only
1775                fitter_single.fitter_id = [self.page_id]
1776            fit_id += 1
1777            fitters.append(fitter_single)
1778
1779        return fitters, fit_id
1780
1781    def iterateOverModel(self, func):
1782        """
1783        Take func and throw it inside the model row loop
1784        """
1785        for row_i in range(self._model_model.rowCount()):
1786            func(row_i)
1787
1788    def updateModelFromList(self, param_dict):
1789        """
1790        Update the model with new parameters, create the errors column
1791        """
1792        assert isinstance(param_dict, dict)
1793        if not dict:
1794            return
1795
1796        def updateFittedValues(row):
1797            # Utility function for main model update
1798            # internal so can use closure for param_dict
1799            param_name = str(self._model_model.item(row, 0).text())
1800            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1801                return
1802            # modify the param value
1803            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1804            self._model_model.item(row, 1).setText(param_repr)
1805            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1806            if self.has_error_column:
1807                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1808                self._model_model.item(row, 2).setText(error_repr)
1809
1810        def updatePolyValues(row):
1811            # Utility function for updateof polydispersity part of the main model
1812            param_name = str(self._model_model.item(row, 0).text())+'.width'
1813            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1814                return
1815            # modify the param value
1816            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1817            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1818            # modify the param error
1819            if self.has_error_column:
1820                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1821                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1822
1823        def createErrorColumn(row):
1824            # Utility function for error column update
1825            item = QtGui.QStandardItem()
1826            def createItem(param_name):
1827                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1828                item.setText(error_repr)
1829            def curr_param():
1830                return str(self._model_model.item(row, 0).text())
1831
1832            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1833
1834            error_column.append(item)
1835
1836        def createPolyErrorColumn(row):
1837            # Utility function for error column update in the polydispersity sub-rows
1838            # NOTE: only creates empty items; updatePolyValues adds the error value
1839            item = self._model_model.item(row, 0)
1840            if not item.hasChildren():
1841                return
1842            poly_item = item.child(0)
1843            if not poly_item.hasChildren():
1844                return
1845            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1846
1847        if not self.has_error_column:
1848            # create top-level error column
1849            error_column = []
1850            self.lstParams.itemDelegate().addErrorColumn()
1851            self.iterateOverModel(createErrorColumn)
1852
1853            self._model_model.insertColumn(2, error_column)
1854
1855            FittingUtilities.addErrorHeadersToModel(self._model_model)
1856
1857            # create error column in polydispersity sub-rows
1858            self.iterateOverModel(createPolyErrorColumn)
1859
1860            self.has_error_column = True
1861
1862        # block signals temporarily, so we don't end up
1863        # updating charts with every single model change on the end of fitting
1864        self._model_model.dataChanged.disconnect()
1865        self.iterateOverModel(updateFittedValues)
1866        self.iterateOverModel(updatePolyValues)
1867        self._model_model.dataChanged.connect(self.onMainParamsChange)
1868
1869    def iterateOverPolyModel(self, func):
1870        """
1871        Take func and throw it inside the poly model row loop
1872        """
1873        for row_i in range(self._poly_model.rowCount()):
1874            func(row_i)
1875
1876    def updatePolyModelFromList(self, param_dict):
1877        """
1878        Update the polydispersity model with new parameters, create the errors column
1879        """
1880        assert isinstance(param_dict, dict)
1881        if not dict:
1882            return
1883
1884        def updateFittedValues(row_i):
1885            # Utility function for main model update
1886            # internal so can use closure for param_dict
1887            if row_i >= self._poly_model.rowCount():
1888                return
1889            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1890            if param_name not in list(param_dict.keys()):
1891                return
1892            # modify the param value
1893            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1894            self._poly_model.item(row_i, 1).setText(param_repr)
1895            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1896            if self.has_poly_error_column:
1897                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1898                self._poly_model.item(row_i, 2).setText(error_repr)
1899
1900        def createErrorColumn(row_i):
1901            # Utility function for error column update
1902            if row_i >= self._poly_model.rowCount():
1903                return
1904            item = QtGui.QStandardItem()
1905
1906            def createItem(param_name):
1907                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1908                item.setText(error_repr)
1909
1910            def poly_param():
1911                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1912
1913            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1914
1915            error_column.append(item)
1916
1917        # block signals temporarily, so we don't end up
1918        # updating charts with every single model change on the end of fitting
1919        self._poly_model.dataChanged.disconnect()
1920        self.iterateOverPolyModel(updateFittedValues)
1921        self._poly_model.dataChanged.connect(self.onPolyModelChange)
1922
1923        if self.has_poly_error_column:
1924            return
1925
1926        self.lstPoly.itemDelegate().addErrorColumn()
1927        error_column = []
1928        self.iterateOverPolyModel(createErrorColumn)
1929
1930        # switch off reponse to model change
1931        self._poly_model.insertColumn(2, error_column)
1932        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1933
1934        self.has_poly_error_column = True
1935
1936    def iterateOverMagnetModel(self, func):
1937        """
1938        Take func and throw it inside the magnet model row loop
1939        """
1940        for row_i in range(self._magnet_model.rowCount()):
1941            func(row_i)
1942
1943    def updateMagnetModelFromList(self, param_dict):
1944        """
1945        Update the magnetic model with new parameters, create the errors column
1946        """
1947        assert isinstance(param_dict, dict)
1948        if not dict:
1949            return
1950        if self._magnet_model.rowCount() == 0:
1951            return
1952
1953        def updateFittedValues(row):
1954            # Utility function for main model update
1955            # internal so can use closure for param_dict
1956            if self._magnet_model.item(row, 0) is None:
1957                return
1958            param_name = str(self._magnet_model.item(row, 0).text())
1959            if param_name not in list(param_dict.keys()):
1960                return
1961            # modify the param value
1962            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1963            self._magnet_model.item(row, 1).setText(param_repr)
1964            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1965            if self.has_magnet_error_column:
1966                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1967                self._magnet_model.item(row, 2).setText(error_repr)
1968
1969        def createErrorColumn(row):
1970            # Utility function for error column update
1971            item = QtGui.QStandardItem()
1972            def createItem(param_name):
1973                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1974                item.setText(error_repr)
1975            def curr_param():
1976                return str(self._magnet_model.item(row, 0).text())
1977
1978            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1979
1980            error_column.append(item)
1981
1982        # block signals temporarily, so we don't end up
1983        # updating charts with every single model change on the end of fitting
1984        self._magnet_model.dataChanged.disconnect()
1985        self.iterateOverMagnetModel(updateFittedValues)
1986        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
1987
1988        if self.has_magnet_error_column:
1989            return
1990
1991        self.lstMagnetic.itemDelegate().addErrorColumn()
1992        error_column = []
1993        self.iterateOverMagnetModel(createErrorColumn)
1994
1995        # switch off reponse to model change
1996        self._magnet_model.insertColumn(2, error_column)
1997        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1998
1999        self.has_magnet_error_column = True
2000
2001    def onPlot(self):
2002        """
2003        Plot the current set of data
2004        """
2005        # Regardless of previous state, this should now be `plot show` functionality only
2006        self.cmdPlot.setText("Show Plot")
2007        # Force data recalculation so existing charts are updated
2008        if not self.data_is_loaded:
2009            self.showTheoryPlot()
2010        else:
2011            self.showPlot()
2012        # This is an important processEvent.
2013        # This allows charts to be properly updated in order
2014        # of plots being applied.
2015        QtWidgets.QApplication.processEvents()
2016        self.recalculatePlotData() # recalc+plot theory again (2nd)
2017
2018    def onSmearingOptionsUpdate(self):
2019        """
2020        React to changes in the smearing widget
2021        """
2022        self.calculateQGridForModel()
2023
2024    def recalculatePlotData(self):
2025        """
2026        Generate a new dataset for model
2027        """
2028        if not self.data_is_loaded:
2029            self.createDefaultDataset()
2030        self.calculateQGridForModel()
2031
2032    def showTheoryPlot(self):
2033        """
2034        Show the current theory plot in MPL
2035        """
2036        # Show the chart if ready
2037        if self.theory_item is None:
2038            self.recalculatePlotData()
2039        elif self.model_data:
2040            self._requestPlots(self.model_data.filename, self.theory_item.model())
2041
2042    def showPlot(self):
2043        """
2044        Show the current plot in MPL
2045        """
2046        # Show the chart if ready
2047        data_to_show = self.data
2048        # Any models for this page
2049        current_index = self.all_data[self.data_index]
2050        item = self._requestPlots(self.data.filename, current_index.model())
2051        if item:
2052            # fit+data has not been shown - show just data
2053            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
2054
2055    def _requestPlots(self, item_name, item_model):
2056        """
2057        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
2058        """
2059        fitpage_name = self.kernel_module.name
2060        plots = GuiUtils.plotsFromFilename(item_name, item_model)
2061        # Has the fitted data been shown?
2062        data_shown = False
2063        item = None
2064        for item, plot in plots.items():
2065            if fitpage_name in plot.name:
2066                data_shown = True
2067                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
2068        # return the last data item seen, if nothing was plotted; supposed to be just data)
2069        return None if data_shown else item
2070
2071    def onOptionsUpdate(self):
2072        """
2073        Update local option values and replot
2074        """
2075        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
2076            self.options_widget.state()
2077        # set Q range labels on the main tab
2078        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2079        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
2080        self.recalculatePlotData()
2081
2082    def setDefaultStructureCombo(self):
2083        """
2084        Fill in the structure factors combo box with defaults
2085        """
2086        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
2087        factors = [factor[0] for factor in structure_factor_list]
2088        factors.insert(0, STRUCTURE_DEFAULT)
2089        self.cbStructureFactor.clear()
2090        self.cbStructureFactor.addItems(sorted(factors))
2091
2092    def createDefaultDataset(self):
2093        """
2094        Generate default Dataset 1D/2D for the given model
2095        """
2096        # Create default datasets if no data passed
2097        if self.is2D:
2098            qmax = self.q_range_max/np.sqrt(2)
2099            qstep = self.npts
2100            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
2101            return
2102        elif self.log_points:
2103            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
2104            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
2105            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
2106        else:
2107            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
2108                                   num=self.npts, endpoint=True)
2109        self.logic.createDefault1dData(interval, self.tab_id)
2110
2111    def readCategoryInfo(self):
2112        """
2113        Reads the categories in from file
2114        """
2115        self.master_category_dict = defaultdict(list)
2116        self.by_model_dict = defaultdict(list)
2117        self.model_enabled_dict = defaultdict(bool)
2118
2119        categorization_file = CategoryInstaller.get_user_file()
2120        if not os.path.isfile(categorization_file):
2121            categorization_file = CategoryInstaller.get_default_file()
2122        with open(categorization_file, 'rb') as cat_file:
2123            self.master_category_dict = json.load(cat_file)
2124            self.regenerateModelDict()
2125
2126        # Load the model dict
2127        models = load_standard_models()
2128        for model in models:
2129            self.models[model.name] = model
2130
2131        self.readCustomCategoryInfo()
2132
2133    def readCustomCategoryInfo(self):
2134        """
2135        Reads the custom model category
2136        """
2137        #Looking for plugins
2138        self.plugins = list(self.custom_models.values())
2139        plugin_list = []
2140        for name, plug in self.custom_models.items():
2141            self.models[name] = plug
2142            plugin_list.append([name, True])
2143        if plugin_list:
2144            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
2145
2146    def regenerateModelDict(self):
2147        """
2148        Regenerates self.by_model_dict which has each model name as the
2149        key and the list of categories belonging to that model
2150        along with the enabled mapping
2151        """
2152        self.by_model_dict = defaultdict(list)
2153        for category in self.master_category_dict:
2154            for (model, enabled) in self.master_category_dict[category]:
2155                self.by_model_dict[model].append(category)
2156                self.model_enabled_dict[model] = enabled
2157
2158    def addBackgroundToModel(self, model):
2159        """
2160        Adds background parameter with default values to the model
2161        """
2162        assert isinstance(model, QtGui.QStandardItemModel)
2163        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2164        FittingUtilities.addCheckedListToModel(model, checked_list)
2165        last_row = model.rowCount()-1
2166        model.item(last_row, 0).setEditable(False)
2167        model.item(last_row, 4).setEditable(False)
2168
2169    def addScaleToModel(self, model):
2170        """
2171        Adds scale parameter with default values to the model
2172        """
2173        assert isinstance(model, QtGui.QStandardItemModel)
2174        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2175        FittingUtilities.addCheckedListToModel(model, checked_list)
2176        last_row = model.rowCount()-1
2177        model.item(last_row, 0).setEditable(False)
2178        model.item(last_row, 4).setEditable(False)
2179
2180    def addWeightingToData(self, data):
2181        """
2182        Adds weighting contribution to fitting data
2183        """
2184        new_data = copy.deepcopy(data)
2185        # Send original data for weighting
2186        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2187        if self.is2D:
2188            new_data.err_data = weight
2189        else:
2190            new_data.dy = weight
2191
2192        return new_data
2193
2194    def updateQRange(self):
2195        """
2196        Updates Q Range display
2197        """
2198        if self.data_is_loaded:
2199            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2200        # set Q range labels on the main tab
2201        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2202        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
2203        # set Q range labels on the options tab
2204        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2205
2206    def SASModelToQModel(self, model_name, structure_factor=None):
2207        """
2208        Setting model parameters into table based on selected category
2209        """
2210        # Crete/overwrite model items
2211        self._model_model.clear()
2212        self._poly_model.clear()
2213        self._magnet_model.clear()
2214
2215        if model_name is None:
2216            if structure_factor not in (None, "None"):
2217                # S(Q) on its own, treat the same as a form factor
2218                self.kernel_module = None
2219                self.fromStructureFactorToQModel(structure_factor)
2220            else:
2221                # No models selected
2222                return
2223        else:
2224            self.fromModelToQModel(model_name)
2225            self.addExtraShells()
2226
2227            # Allow the SF combobox visibility for the given sasmodel
2228            self.enableStructureFactorControl(structure_factor)
2229       
2230            # Add S(Q)
2231            if self.cbStructureFactor.isEnabled():
2232                structure_factor = self.cbStructureFactor.currentText()
2233                self.fromStructureFactorToQModel(structure_factor)
2234
2235            # Add polydispersity to the model
2236            self.poly_params = {}
2237            self.setPolyModel()
2238            # Add magnetic parameters to the model
2239            self.magnet_params = {}
2240            self.setMagneticModel()
2241
2242        # Now we claim the model has been loaded
2243        self.model_is_loaded = True
2244        # Change the model name to a monicker
2245        self.kernel_module.name = self.modelName()
2246        # Update the smearing tab
2247        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2248
2249        # (Re)-create headers
2250        FittingUtilities.addHeadersToModel(self._model_model)
2251        self.lstParams.header().setFont(self.boldFont)
2252
2253        # Update Q Ranges
2254        self.updateQRange()
2255
2256    def fromModelToQModel(self, model_name):
2257        """
2258        Setting model parameters into QStandardItemModel based on selected _model_
2259        """
2260        name = model_name
2261        kernel_module = None
2262        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2263            # custom kernel load requires full path
2264            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2265        try:
2266            kernel_module = generate.load_kernel_module(name)
2267        except ModuleNotFoundError as ex:
2268            pass
2269        except FileNotFoundError as ex:
2270            # can happen when name attribute not the same as actual filename
2271            pass
2272
2273        if kernel_module is None:
2274            # mismatch between "name" attribute and actual filename.
2275            curr_model = self.models[model_name]
2276            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2277            try:
2278                kernel_module = generate.load_kernel_module(name)
2279            except ModuleNotFoundError as ex:
2280                logger.error("Can't find the model "+ str(ex))
2281                return
2282
2283        if hasattr(kernel_module, 'parameters'):
2284            # built-in and custom models
2285            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2286
2287        elif hasattr(kernel_module, 'model_info'):
2288            # for sum/multiply models
2289            self.model_parameters = kernel_module.model_info.parameters
2290
2291        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2292            # this probably won't work if there's no model_info, but just in case
2293            self.model_parameters = kernel_module.Model._model_info.parameters
2294        else:
2295            # no parameters - default to blank table
2296            msg = "No parameters found in model '{}'.".format(model_name)
2297            logger.warning(msg)
2298            self.model_parameters = modelinfo.ParameterTable([])
2299
2300        # Instantiate the current sasmodel
2301        self.kernel_module = self.models[model_name]()
2302
2303        # Change the model name to a monicker
2304        self.kernel_module.name = self.modelName()
2305
2306        # Explicitly add scale and background with default values
2307        temp_undo_state = self.undo_supported
2308        self.undo_supported = False
2309        self.addScaleToModel(self._model_model)
2310        self.addBackgroundToModel(self._model_model)
2311        self.undo_supported = temp_undo_state
2312
2313        self.shell_names = self.shellNamesList()
2314
2315        # Add heading row
2316        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2317
2318        # Update the QModel
2319        FittingUtilities.addParametersToModel(
2320                self.model_parameters,
2321                self.kernel_module,
2322                self.is2D,
2323                self._model_model,
2324                self.lstParams)
2325
2326    def fromStructureFactorToQModel(self, structure_factor):
2327        """
2328        Setting model parameters into QStandardItemModel based on selected _structure factor_
2329        """
2330        if structure_factor is None or structure_factor=="None":
2331            return
2332
2333        product_params = None
2334
2335        if self.kernel_module is None:
2336            # Structure factor is the only selected model; build it and show all its params
2337            self.kernel_module = self.models[structure_factor]()
2338            self.kernel_module.name = self.modelName()
2339            s_params = self.kernel_module._model_info.parameters
2340            s_params_orig = s_params
2341        else:
2342            s_kernel = self.models[structure_factor]()
2343            p_kernel = self.kernel_module
2344            # need to reset multiplicity to get the right product
2345            if p_kernel.is_multiplicity_model:
2346                p_kernel.multiplicity = p_kernel.multiplicity_info.number
2347
2348            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2349            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2350
2351            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2352            # Modify the name to correspond to shown items
2353            self.kernel_module.name = self.modelName()
2354            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2355            all_param_names = [param.name for param in all_params]
2356
2357            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2358            # conflicting names with P(Q) params will cause a rename
2359
2360            if "radius_effective_mode" in all_param_names:
2361                # Show all parameters
2362                # In this case, radius_effective is NOT pruned by sasmodels.product
2363                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2364                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2365                product_params = modelinfo.ParameterTable(
2366                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2367            else:
2368                # Ensure radius_effective is not displayed
2369                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2370                if "radius_effective" in all_param_names:
2371                    # In this case, radius_effective is NOT pruned by sasmodels.product
2372                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2373                    product_params = modelinfo.ParameterTable(
2374                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2375                else:
2376                    # In this case, radius_effective is pruned by sasmodels.product
2377                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2378                    product_params = modelinfo.ParameterTable(
2379                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2380
2381        # Add heading row
2382        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2383
2384        # Get new rows for QModel
2385        # Any renamed parameters are stored as data in the relevant item, for later handling
2386        FittingUtilities.addSimpleParametersToModel(
2387                parameters=s_params,
2388                is2D=self.is2D,
2389                parameters_original=s_params_orig,
2390                model=self._model_model,
2391                view=self.lstParams)
2392
2393        # Insert product-only params into QModel
2394        if product_params:
2395            prod_rows = FittingUtilities.addSimpleParametersToModel(
2396                    parameters=product_params,
2397                    is2D=self.is2D,
2398                    parameters_original=None,
2399                    model=self._model_model,
2400                    view=self.lstParams,
2401                    row_num=2)
2402
2403            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2404            self._n_shells_row += len(prod_rows)
2405
2406    def haveParamsToFit(self):
2407        """
2408        Finds out if there are any parameters ready to be fitted
2409        """
2410        if not self.logic.data_is_loaded:
2411            return False
2412        if self.main_params_to_fit:
2413            return True
2414        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2415            return True
2416        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self.magnet_params_to_fit:
2417            return True
2418        return False
2419
2420    def onMainParamsChange(self, top, bottom):
2421        """
2422        Callback method for updating the sasmodel parameters with the GUI values
2423        """
2424        item = self._model_model.itemFromIndex(top)
2425
2426        model_column = item.column()
2427
2428        if model_column == 0:
2429            self.checkboxSelected(item)
2430            self.cmdFit.setEnabled(self.haveParamsToFit())
2431            # Update state stack
2432            self.updateUndo()
2433            return
2434
2435        model_row = item.row()
2436        name_index = self._model_model.index(model_row, 0)
2437        name_item = self._model_model.itemFromIndex(name_index)
2438
2439        # Extract changed value.
2440        try:
2441            value = GuiUtils.toDouble(item.text())
2442        except TypeError:
2443            # Unparsable field
2444            return
2445
2446        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2447        if name_item.data(QtCore.Qt.UserRole):
2448            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2449        else:
2450            parameter_name = str(self._model_model.data(name_index))
2451
2452        # Update the parameter value - note: this supports +/-inf as well
2453        param_column = self.lstParams.itemDelegate().param_value
2454        min_column = self.lstParams.itemDelegate().param_min
2455        max_column = self.lstParams.itemDelegate().param_max
2456        if model_column == param_column:
2457            # don't try to update multiplicity counters if they aren't there.
2458            # Note that this will fail for proper bad update where the model
2459            # doesn't contain multiplicity parameter
2460            if parameter_name != self.kernel_module.multiplicity_info.control:
2461                self.kernel_module.setParam(parameter_name, value)
2462        elif model_column == min_column:
2463            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2464            self.kernel_module.details[parameter_name][1] = value
2465        elif model_column == max_column:
2466            self.kernel_module.details[parameter_name][2] = value
2467        else:
2468            # don't update the chart
2469            return
2470
2471        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2472        # TODO: multishell params in self.kernel_module.details[??] = value
2473
2474        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2475        # necessary
2476        self.processEffectiveRadius()
2477
2478        # Force the chart update when actual parameters changed
2479        if model_column == 1:
2480            self.recalculatePlotData()
2481
2482        # Update state stack
2483        self.updateUndo()
2484
2485    def processEffectiveRadius(self):
2486        """
2487        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2488        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2489        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2490
2491        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2492        displayed for the user; that is not dealt with here, but in complete1D.
2493        """
2494        ER_row = self.getRowFromName("radius_effective")
2495        if ER_row is None:
2496            return
2497
2498        ER_mode_row = self.getRowFromName("radius_effective_mode")
2499        if ER_mode_row is None:
2500            return
2501
2502        try:
2503            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2504        except ValueError:
2505            logging.error("radius_effective_mode was set to an invalid value.")
2506            return
2507
2508        if ER_mode == 0:
2509            # ensure the ER value can be modified by user
2510            self.setParamEditableByRow(ER_row, True)
2511        elif ER_mode > 0:
2512            # ensure the ER value cannot be modified by user
2513            self.setParamEditableByRow(ER_row, False)
2514        else:
2515            logging.error("radius_effective_mode was set to an invalid value.")
2516
2517    def setParamEditableByRow(self, row, editable=True):
2518        """
2519        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2520        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2521        """
2522        item_name = self._model_model.item(row, 0)
2523        item_value = self._model_model.item(row, 1)
2524
2525        item_value.setEditable(editable)
2526
2527        if editable:
2528            # reset font
2529            item_name.setFont(QtGui.QFont())
2530            # reset colour
2531            item_name.setForeground(QtGui.QBrush())
2532            # make checkable
2533            item_name.setCheckable(True)
2534        else:
2535            # change font
2536            font = QtGui.QFont()
2537            font.setItalic(True)
2538            item_name.setFont(font)
2539            # change colour
2540            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2541            # make not checkable (and uncheck)
2542            item_name.setCheckState(QtCore.Qt.Unchecked)
2543            item_name.setCheckable(False)
2544
2545    def isCheckable(self, row):
2546        return self._model_model.item(row, 0).isCheckable()
2547
2548    def selectCheckbox(self, row):
2549        """
2550        Select the checkbox in given row.
2551        """
2552        assert 0<= row <= self._model_model.rowCount()
2553        index = self._model_model.index(row, 0)
2554        item = self._model_model.itemFromIndex(index)
2555        item.setCheckState(QtCore.Qt.Checked)
2556
2557    def checkboxSelected(self, item):
2558        # Assure we're dealing with checkboxes
2559        if not item.isCheckable():
2560            return
2561        status = item.checkState()
2562
2563        # If multiple rows selected - toggle all of them, filtering uncheckable
2564        # Convert to proper indices and set requested enablement
2565        self.setParameterSelection(status)
2566
2567        # update the list of parameters to fit
2568        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2569
2570    def checkedListFromModel(self, model):
2571        """
2572        Returns list of checked parameters for given model
2573        """
2574        def isChecked(row):
2575            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2576
2577        return [str(model.item(row_index, 0).text())
2578                for row_index in range(model.rowCount())
2579                if isChecked(row_index)]
2580
2581    def createNewIndex(self, fitted_data):
2582        """
2583        Create a model or theory index with passed Data1D/Data2D
2584        """
2585        if self.data_is_loaded:
2586            if not fitted_data.name:
2587                name = self.nameForFittedData(self.data.filename)
2588                fitted_data.title = name
2589                fitted_data.name = name
2590                fitted_data.filename = name
2591                fitted_data.symbol = "Line"
2592            self.updateModelIndex(fitted_data)
2593        else:
2594            if not fitted_data.name:
2595                name = self.nameForFittedData(self.kernel_module.id)
2596            else:
2597                name = fitted_data.name
2598            fitted_data.title = name
2599            fitted_data.filename = name
2600            fitted_data.symbol = "Line"
2601            self.createTheoryIndex(fitted_data)
2602            # Switch to the theory tab for user's glee
2603            self.communicate.changeDataExplorerTabSignal.emit(1)
2604
2605    def updateModelIndex(self, fitted_data):
2606        """
2607        Update a QStandardModelIndex containing model data
2608        """
2609        name = self.nameFromData(fitted_data)
2610        # Make this a line if no other defined
2611        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2612            fitted_data.symbol = 'Line'
2613        # Notify the GUI manager so it can update the main model in DataExplorer
2614        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2615
2616    def createTheoryIndex(self, fitted_data):
2617        """
2618        Create a QStandardModelIndex containing model data
2619        """
2620        name = self.nameFromData(fitted_data)
2621        # Modify the item or add it if new
2622        theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2623        self.communicate.updateTheoryFromPerspectiveSignal.emit(theory_item)
2624
2625    def setTheoryItem(self, item):
2626        """
2627        Reset the theory item based on the data explorer update
2628        """
2629        self.theory_item = item
2630
2631    def nameFromData(self, fitted_data):
2632        """
2633        Return name for the dataset. Terribly impure function.
2634        """
2635        if fitted_data.name is None:
2636            name = self.nameForFittedData(self.logic.data.filename)
2637            fitted_data.title = name
2638            fitted_data.name = name
2639            fitted_data.filename = name
2640        else:
2641            name = fitted_data.name
2642        return name
2643
2644    def methodCalculateForData(self):
2645        '''return the method for data calculation'''
2646        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2647
2648    def methodCompleteForData(self):
2649        '''return the method for result parsin on calc complete '''
2650        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2651
2652    def updateKernelModelWithExtraParams(self, model=None):
2653        """
2654        Updates kernel model 'model' with extra parameters from
2655        the polydisp and magnetism tab, if the tabs are enabled
2656        """
2657        if model is None: return
2658        if not hasattr(model, 'setParam'): return
2659
2660        # add polydisperse parameters if asked
2661        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2662            for key, value in self.poly_params.items():
2663                model.setParam(key, value)
2664        # add magnetic params if asked
2665        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
2666            for key, value in self.magnet_params.items():
2667                model.setParam(key, value)
2668
2669    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2670        """
2671        Wrapper for Calc1D/2D calls
2672        """
2673        if data is None:
2674            data = self.data
2675        if model is None:
2676            model = copy.deepcopy(self.kernel_module)
2677            self.updateKernelModelWithExtraParams(model)
2678
2679        if completefn is None:
2680            completefn = self.methodCompleteForData()
2681        smearer = self.smearing_widget.smearer()
2682        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2683
2684        # Disable buttons/table
2685        self.disableInteractiveElementsOnCalculate()
2686        # Awful API to a backend method.
2687        calc_thread = self.methodCalculateForData()(data=data,
2688                                               model=model,
2689                                               page_id=0,
2690                                               qmin=self.q_range_min,
2691                                               qmax=self.q_range_max,
2692                                               smearer=smearer,
2693                                               state=None,
2694                                               weight=weight,
2695                                               fid=None,
2696                                               toggle_mode_on=False,
2697                                               completefn=completefn,
2698                                               update_chisqr=True,
2699                                               exception_handler=self.calcException,
2700                                               source=None)
2701        if use_threads:
2702            if LocalConfig.USING_TWISTED:
2703                # start the thread with twisted
2704                thread = threads.deferToThread(calc_thread.compute)
2705                thread.addCallback(completefn)
2706                thread.addErrback(self.calculateDataFailed)
2707            else:
2708                # Use the old python threads + Queue
2709                calc_thread.queue()
2710                calc_thread.ready(2.5)
2711        else:
2712            results = calc_thread.compute()
2713            completefn(results)
2714
2715    def calculateQGridForModel(self):
2716        """
2717        Prepare the fitting data object, based on current ModelModel
2718        """
2719        if self.kernel_module is None:
2720            return
2721        self.calculateQGridForModelExt()
2722
2723    def calculateDataFailed(self, reason):
2724        """
2725        Thread returned error
2726        """
2727        # Bring the GUI to normal state
2728        self.enableInteractiveElements()
2729        print("Calculate Data failed with ", reason)
2730
2731    def completed1D(self, return_data):
2732        self.Calc1DFinishedSignal.emit(return_data)
2733
2734    def completed2D(self, return_data):
2735        self.Calc2DFinishedSignal.emit(return_data)
2736
2737    def complete1D(self, return_data):
2738        """
2739        Plot the current 1D data
2740        """
2741        # Bring the GUI to normal state
2742        self.enableInteractiveElements()
2743        if return_data is None:
2744            return
2745        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2746
2747        # assure the current index is set properly for batch
2748        if len(self._logic) > 1:
2749            for i, logic in enumerate(self._logic):
2750                if logic.data.name in fitted_data.name:
2751                    self.data_index = i
2752
2753        residuals = self.calculateResiduals(fitted_data)
2754        self.model_data = fitted_data
2755        new_plots = [fitted_data]
2756        if residuals is not None:
2757            new_plots.append(residuals)
2758
2759        if self.data_is_loaded:
2760            # delete any plots associated with the data that were not updated
2761            # (e.g. to remove beta(Q), S_eff(Q))
2762            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2763            pass
2764        else:
2765            # delete theory items for the model, in order to get rid of any
2766            # redundant items, e.g. beta(Q), S_eff(Q)
2767            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2768
2769        # Create plots for parameters with enabled polydispersity
2770        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2771            data_id = fitted_data.id.split()
2772            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2773            data_name = fitted_data.name.split()
2774            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2775            self.createNewIndex(plot)
2776            new_plots.append(plot)
2777
2778        # Create plots for intermediate product data
2779        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2780        for plot in plots:
2781            plot.symbol = "Line"
2782            self.createNewIndex(plot)
2783            new_plots.append(plot)
2784
2785        for plot in new_plots:
2786            self.communicate.plotUpdateSignal.emit([plot])
2787
2788        # Update radius_effective if relevant
2789        self.updateEffectiveRadius(return_data)
2790
2791    def complete2D(self, return_data):
2792        """
2793        Plot the current 2D data
2794        """
2795        # Bring the GUI to normal state
2796        self.enableInteractiveElements()
2797
2798        if return_data is None:
2799            return
2800
2801        fitted_data = self.logic.new2DPlot(return_data)
2802        # assure the current index is set properly for batch
2803        if len(self._logic) > 1:
2804            for i, logic in enumerate(self._logic):
2805                if logic.data.name in fitted_data.name:
2806                    self.data_index = i
2807
2808        residuals = self.calculateResiduals(fitted_data)
2809        self.model_data = fitted_data
2810        new_plots = [fitted_data]
2811        if residuals is not None:
2812            new_plots.append(residuals)
2813
2814        # Update/generate plots
2815        for plot in new_plots:
2816            self.communicate.plotUpdateSignal.emit([plot])
2817
2818    def updateEffectiveRadius(self, return_data):
2819        """
2820        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2821        calculated value as returned by sasmodels (if the value was returned).
2822        """
2823        ER_mode_row = self.getRowFromName("radius_effective_mode")
2824        if ER_mode_row is None:
2825            return
2826        try:
2827            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2828        except ValueError:
2829            logging.error("radius_effective_mode was set to an invalid value.")
2830            return
2831        if ER_mode < 1:
2832            # does not need updating if it is not being computed
2833            return
2834
2835        ER_row = self.getRowFromName("radius_effective")
2836        if ER_row is None:
2837            return
2838
2839        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2840        ER_value = scalar_results.get("effective_radius") # note name of key
2841        if ER_value is None:
2842            return
2843        # ensure the model does not recompute when updating the value
2844        self._model_model.blockSignals(True)
2845        self._model_model.item(ER_row, 1).setText(str(ER_value))
2846        self._model_model.blockSignals(False)
2847        # ensure the view is updated immediately
2848        self._model_model.layoutChanged.emit()
2849
2850    def calculateResiduals(self, fitted_data):
2851        """
2852        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2853        """
2854        # Create a new index for holding data
2855        fitted_data.symbol = "Line"
2856
2857        # Modify fitted_data with weighting
2858        weighted_data = self.addWeightingToData(fitted_data)
2859
2860        self.createNewIndex(weighted_data)
2861
2862        # Calculate difference between return_data and logic.data
2863        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2864        # Update the control
2865        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2866        self.lblChi2Value.setText(chi2_repr)
2867
2868        # Plot residuals if actual data
2869        if not self.data_is_loaded:
2870            return
2871
2872        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2873        if residuals_plot is None:
2874            return
2875        residuals_plot.id = "Residual " + residuals_plot.id
2876        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2877        self.createNewIndex(residuals_plot)
2878        return residuals_plot
2879
2880    def onCategoriesChanged(self):
2881            """
2882            Reload the category/model comboboxes
2883            """
2884            # Store the current combo indices
2885            current_cat = self.cbCategory.currentText()
2886            current_model = self.cbModel.currentText()
2887
2888            # reread the category file and repopulate the combo
2889            self.cbCategory.blockSignals(True)
2890            self.cbCategory.clear()
2891            self.readCategoryInfo()
2892            self.initializeCategoryCombo()
2893
2894            # Scroll back to the original index in Categories
2895            new_index = self.cbCategory.findText(current_cat)
2896            if new_index != -1:
2897                self.cbCategory.setCurrentIndex(new_index)
2898            self.cbCategory.blockSignals(False)
2899            # ...and in the Models
2900            self.cbModel.blockSignals(True)
2901            new_index = self.cbModel.findText(current_model)
2902            if new_index != -1:
2903                self.cbModel.setCurrentIndex(new_index)
2904            self.cbModel.blockSignals(False)
2905
2906            return
2907
2908    def calcException(self, etype, value, tb):
2909        """
2910        Thread threw an exception.
2911        """
2912        # Bring the GUI to normal state
2913        self.enableInteractiveElements()
2914        # TODO: remimplement thread cancellation
2915        logger.error("".join(traceback.format_exception(etype, value, tb)))
2916
2917    def setTableProperties(self, table):
2918        """
2919        Setting table properties
2920        """
2921        # Table properties
2922        table.verticalHeader().setVisible(False)
2923        table.setAlternatingRowColors(True)
2924        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2925        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2926        table.resizeColumnsToContents()
2927
2928        # Header
2929        header = table.horizontalHeader()
2930        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2931        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2932
2933        # Qt5: the following 2 lines crash - figure out why!
2934        # Resize column 0 and 7 to content
2935        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2936        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2937
2938    def setPolyModel(self):
2939        """
2940        Set polydispersity values
2941        """
2942        if not self.model_parameters:
2943            return
2944        self._poly_model.clear()
2945
2946        parameters = self.model_parameters.form_volume_parameters
2947        if self.is2D:
2948            parameters += self.model_parameters.orientation_parameters
2949
2950        [self.setPolyModelParameters(i, param) for i, param in \
2951            enumerate(parameters) if param.polydisperse]
2952
2953        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2954
2955    def setPolyModelParameters(self, i, param):
2956        """
2957        Standard of multishell poly parameter driver
2958        """
2959        param_name = param.name
2960        # see it the parameter is multishell
2961        if '[' in param.name:
2962            # Skip empty shells
2963            if self.current_shell_displayed == 0:
2964                return
2965            else:
2966                # Create as many entries as current shells
2967                for ishell in range(1, self.current_shell_displayed+1):
2968                    # Remove [n] and add the shell numeral
2969                    name = param_name[0:param_name.index('[')] + str(ishell)
2970                    self.addNameToPolyModel(i, name)
2971        else:
2972            # Just create a simple param entry
2973            self.addNameToPolyModel(i, param_name)
2974
2975    def addNameToPolyModel(self, i, param_name):
2976        """
2977        Creates a checked row in the poly model with param_name
2978        """
2979        # Polydisp. values from the sasmodel
2980        width = self.kernel_module.getParam(param_name + '.width')
2981        npts = self.kernel_module.getParam(param_name + '.npts')
2982        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2983        _, min, max = self.kernel_module.details[param_name]
2984
2985        # Update local param dict
2986        self.poly_params[param_name + '.width'] = width
2987        self.poly_params[param_name + '.npts'] = npts
2988        self.poly_params[param_name + '.nsigmas'] = nsigs
2989
2990        # Construct a row with polydisp. related variable.
2991        # This will get added to the polydisp. model
2992        # Note: last argument needs extra space padding for decent display of the control
2993        checked_list = ["Distribution of " + param_name, str(width),
2994                        str(min), str(max),
2995                        str(npts), str(nsigs), "gaussian      ",'']
2996        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2997
2998        # All possible polydisp. functions as strings in combobox
2999        func = QtWidgets.QComboBox()
3000        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
3001        # Set the default index
3002        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
3003        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
3004        self.lstPoly.setIndexWidget(ind, func)
3005        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
3006
3007    def onPolyFilenameChange(self, row_index):
3008        """
3009        Respond to filename_updated signal from the delegate
3010        """
3011        # For the given row, invoke the "array" combo handler
3012        array_caption = 'array'
3013
3014        # Get the combo box reference
3015        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
3016        widget = self.lstPoly.indexWidget(ind)
3017
3018        # Update the combo box so it displays "array"
3019        widget.blockSignals(True)
3020        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
3021        widget.blockSignals(False)
3022
3023        # Invoke the file reader
3024        self.onPolyComboIndexChange(array_caption, row_index)
3025
3026    def onPolyComboIndexChange(self, combo_string, row_index):
3027        """
3028        Modify polydisp. defaults on function choice
3029        """
3030        # Get npts/nsigs for current selection
3031        param = self.model_parameters.form_volume_parameters[row_index]
3032        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
3033        combo_box = self.lstPoly.indexWidget(file_index)
3034        try:
3035            self.disp_model = POLYDISPERSITY_MODELS[combo_string]()
3036        except IndexError:
3037            logger.error("Error in setting the dispersion model. Reverting to Gaussian.")
3038            self.disp_model = POLYDISPERSITY_MODELS['gaussian']()
3039
3040        def updateFunctionCaption(row):
3041            # Utility function for update of polydispersity function name in the main model
3042            if not self.isCheckable(row):
3043                return
3044            param_name = self._model_model.item(row, 0).text()
3045            if param_name !=  param.name:
3046                return
3047            # Modify the param value
3048            self._model_model.blockSignals(True)
3049            if self.has_error_column:
3050                # err column changes the indexing
3051                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
3052            else:
3053                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
3054            self._model_model.blockSignals(False)
3055
3056        if combo_string == 'array':
3057            try:
3058                # assure the combo is at the right index
3059                combo_box.blockSignals(True)
3060                combo_box.setCurrentIndex(combo_box.findText(combo_string))
3061                combo_box.blockSignals(False)
3062                # Load the file
3063                self.loadPolydispArray(row_index)
3064                # Update main model for display
3065                self.iterateOverModel(updateFunctionCaption)
3066                self.kernel_module.set_dispersion(param.name, self.disp_model)
3067                # uncheck the parameter
3068                self._poly_model.item(row_index, 0).setCheckState(QtCore.Qt.Unchecked)
3069                # disable the row
3070                lo = self.lstPoly.itemDelegate().poly_parameter
3071                hi = self.lstPoly.itemDelegate().poly_function
3072                self._poly_model.blockSignals(True)
3073                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
3074                self._poly_model.blockSignals(False)
3075                return
3076            except IOError:
3077                combo_box.setCurrentIndex(self.orig_poly_index)
3078                # Pass for cancel/bad read
3079                pass
3080        else:
3081            self.kernel_module.set_dispersion(param.name, self.disp_model)
3082
3083        # Enable the row in case it was disabled by Array
3084        self._poly_model.blockSignals(True)
3085        max_range = self.lstPoly.itemDelegate().poly_filename
3086        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
3087        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
3088        self._poly_model.setData(file_index, "")
3089        self._poly_model.blockSignals(False)
3090
3091        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
3092        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
3093
3094        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
3095        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
3096
3097        self._poly_model.setData(npts_index, npts)
3098        self._poly_model.setData(nsigs_index, nsigs)
3099
3100        self.iterateOverModel(updateFunctionCaption)
3101        self.orig_poly_index = combo_box.currentIndex()
3102
3103    def loadPolydispArray(self, row_index):
3104        """
3105        Show the load file dialog and loads requested data into state
3106        """
3107        datafile = QtWidgets.QFileDialog.getOpenFileName(
3108            self, "Choose a weight file", "", "All files (*.*)", None,
3109            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
3110
3111        if not datafile:
3112            logger.info("No weight data chosen.")
3113            raise IOError
3114
3115        values = []
3116        weights = []
3117        def appendData(data_tuple):
3118            """
3119            Fish out floats from a tuple of strings
3120            """
3121            try:
3122                values.append(float(data_tuple[0]))
3123                weights.append(float(data_tuple[1]))
3124            except (ValueError, IndexError):
3125                # just pass through if line with bad data
3126                return
3127
3128        with open(datafile, 'r') as column_file:
3129            column_data = [line.rstrip().split() for line in column_file.readlines()]
3130            [appendData(line) for line in column_data]
3131
3132        # If everything went well - update the sasmodel values
3133        self.disp_model.set_weights(np.array(values), np.array(weights))
3134        # + update the cell with filename
3135        fname = os.path.basename(str(datafile))
3136        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
3137        self._poly_model.setData(fname_index, fname)
3138
3139    def onColumnWidthUpdate(self, index, old_size, new_size):
3140        """
3141        Simple state update of the current column widths in the  param list
3142        """
3143        self.lstParamHeaderSizes[index] = new_size
3144
3145    def setMagneticModel(self):
3146        """
3147        Set magnetism values on model
3148        """
3149        if not self.model_parameters:
3150            return
3151        self._magnet_model.clear()
3152        # default initial value
3153        m0 = 0.5
3154        for param in self.model_parameters.call_parameters:
3155            if param.type != 'magnetic': continue
3156            if "M0" in param.name:
3157                m0 += 0.5
3158                value = m0
3159            else:
3160                value = param.default
3161            self.addCheckedMagneticListToModel(param, value)
3162
3163        FittingUtilities.addHeadersToModel(self._magnet_model)
3164
3165    def shellNamesList(self):
3166        """
3167        Returns list of names of all multi-shell parameters
3168        E.g. for sld[n], radius[n], n=1..3 it will return
3169        [sld1, sld2, sld3, radius1, radius2, radius3]
3170        """
3171        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3172        top_index = self.kernel_module.multiplicity_info.number
3173        shell_names = []
3174        for i in range(1, top_index+1):
3175            for name in multi_names:
3176                shell_names.append(name+str(i))
3177        return shell_names
3178
3179    def addCheckedMagneticListToModel(self, param, value):
3180        """
3181        Wrapper for model update with a subset of magnetic parameters
3182        """
3183        try:
3184            basename, _ = param.name.rsplit('_', 1)
3185        except ValueError:
3186            basename = param.name
3187        if basename in self.shell_names:
3188            try:
3189                shell_index = int(basename[-2:])
3190            except ValueError:
3191                shell_index = int(basename[-1:])
3192
3193            if shell_index > self.current_shell_displayed:
3194                return
3195
3196        checked_list = [param.name,
3197                        str(value),
3198                        str(param.limits[0]),
3199                        str(param.limits[1]),
3200                        param.units]
3201
3202        self.magnet_params[param.name] = value
3203
3204        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
3205
3206    def enableStructureFactorControl(self, structure_factor):
3207        """
3208        Add structure factors to the list of parameters
3209        """
3210        if self.kernel_module.is_form_factor or structure_factor == 'None':
3211            self.enableStructureCombo()
3212        else:
3213            self.disableStructureCombo()
3214
3215    def addExtraShells(self):
3216        """
3217        Add a combobox for multiple shell display
3218        """
3219        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3220
3221        if param_length == 0:
3222            return
3223
3224        # cell 1: variable name
3225        item1 = QtGui.QStandardItem(param_name)
3226
3227        func = QtWidgets.QComboBox()
3228
3229        # cell 2: combobox
3230        item2 = QtGui.QStandardItem()
3231
3232        # cell 3: min value
3233        item3 = QtGui.QStandardItem()
3234        # set the cell to be non-editable
3235        item3.setFlags(item3.flags() ^ QtCore.Qt.ItemIsEditable)
3236
3237        # cell 4: max value
3238        item4 = QtGui.QStandardItem()
3239        # set the cell to be non-editable
3240        item4.setFlags(item4.flags() ^ QtCore.Qt.ItemIsEditable)
3241
3242        # cell 4: SLD button
3243        item5 = QtGui.QStandardItem()
3244        button = QtWidgets.QPushButton()
3245        button.setText("Show SLD Profile")
3246
3247        self._model_model.appendRow([item1, item2, item3, item4, item5])
3248
3249        # Beautify the row:  span columns 2-4
3250        shell_row = self._model_model.rowCount()
3251        shell_index = self._model_model.index(shell_row-1, 1)
3252        button_index = self._model_model.index(shell_row-1, 4)
3253
3254        self.lstParams.setIndexWidget(shell_index, func)
3255        self.lstParams.setIndexWidget(button_index, button)
3256        self._n_shells_row = shell_row - 1
3257
3258        # Get the default number of shells for the model
3259        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3260        shell_par = None
3261        for par in kernel_pars:
3262            parname = par.name
3263            if '[' in parname:
3264                 parname = parname[:parname.index('[')]
3265            if parname == param_name:
3266                shell_par = par
3267                break
3268        if shell_par is None:
3269            logger.error("Could not find %s in kernel parameters.", param_name)
3270            return
3271        default_shell_count = shell_par.default
3272        shell_min = 0
3273        shell_max = 0
3274        try:
3275            shell_min = int(shell_par.limits[0])
3276            shell_max = int(shell_par.limits[1])
3277        except IndexError as ex:
3278            # no info about limits
3279            pass
3280        except OverflowError:
3281            # Try to limit shell_par, if possible
3282            if float(shell_par.limits[1])==np.inf:
3283                shell_max = 9
3284            logging.warning("Limiting shell count to 9.")
3285        except Exception as ex:
3286            logging.error("Badly defined multiplicity: "+ str(ex))
3287            return
3288        # don't update the kernel here - this data is display only
3289        self._model_model.blockSignals(True)
3290        item3.setText(str(shell_min))
3291        item4.setText(str(shell_max))
3292        self._model_model.blockSignals(False)
3293
3294        ## Respond to index change
3295        #func.currentTextChanged.connect(self.modifyShellsInList)
3296
3297        # Respond to button press
3298        button.clicked.connect(self.onShowSLDProfile)
3299
3300        # Available range of shells displayed in the combobox
3301        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3302
3303        # Respond to index change
3304        func.currentTextChanged.connect(self.modifyShellsInList)
3305
3306        # Add default number of shells to the model
3307        func.setCurrentText(str(default_shell_count))
3308        self.modifyShellsInList(str(default_shell_count))
3309
3310    def modifyShellsInList(self, text):
3311        """
3312        Add/remove additional multishell parameters
3313        """
3314        # Find row location of the combobox
3315        first_row = self._n_shells_row + 1
3316        remove_rows = self._num_shell_params
3317        try:
3318            index = int(text)
3319        except ValueError:
3320            # bad text on the control!
3321            index = 0
3322            logger.error("Multiplicity incorrect! Setting to 0")
3323        self.kernel_module.multiplicity = index
3324        if remove_rows > 1:
3325            self._model_model.removeRows(first_row, remove_rows)
3326
3327        new_rows = FittingUtilities.addShellsToModel(
3328                self.model_parameters,
3329                self._model_model,
3330                index,
3331                first_row,
3332                self.lstParams)
3333
3334        self._num_shell_params = len(new_rows)
3335        self.current_shell_displayed = index
3336
3337        # Param values for existing shells were reset to default; force all changes into kernel module
3338        for row in new_rows:
3339            par = row[0].text()
3340            val = GuiUtils.toDouble(row[1].text())
3341            self.kernel_module.setParam(par, val)
3342
3343        # Change 'n' in the parameter model; also causes recalculation
3344        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3345
3346        # Update relevant models
3347        self.setPolyModel()
3348        if self.canHaveMagnetism():
3349            self.setMagneticModel()
3350
3351    def onShowSLDProfile(self):
3352        """
3353        Show a quick plot of SLD profile
3354        """
3355        # get profile data
3356        try:
3357            x, y = self.kernel_module.getProfile()
3358        except TypeError:
3359            msg = "SLD profile calculation failed."
3360            logging.error(msg)
3361            return
3362
3363        y *= 1.0e6
3364        profile_data = Data1D(x=x, y=y)
3365        profile_data.name = "SLD"
3366        profile_data.scale = 'linear'
3367        profile_data.symbol = 'Line'
3368        profile_data.hide_error = True
3369        profile_data._xaxis = "R(\AA)"
3370        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3371
3372        plotter = PlotterWidget(self, quickplot=True)
3373        plotter.data = profile_data
3374        plotter.showLegend = True
3375        plotter.plot(hide_error=True, marker='-')
3376
3377        self.plot_widget = QtWidgets.QWidget()
3378        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3379        layout = QtWidgets.QVBoxLayout()
3380        layout.addWidget(plotter)
3381        self.plot_widget.setLayout(layout)
3382        self.plot_widget.show()
3383
3384    def setInteractiveElements(self, enabled=True):
3385        """
3386        Switch interactive GUI elements on/off
3387        """
3388        assert isinstance(enabled, bool)
3389
3390        self.lstParams.setEnabled(enabled)
3391        self.lstPoly.setEnabled(enabled)
3392        self.lstMagnetic.setEnabled(enabled)
3393
3394        self.cbCategory.setEnabled(enabled)
3395
3396        if enabled:
3397            # worry about original enablement of model and SF
3398            self.cbModel.setEnabled(self.enabled_cbmodel)
3399            self.cbStructureFactor.setEnabled(self.enabled_sfmodel)
3400        else:
3401            self.cbModel.setEnabled(enabled)
3402            self.cbStructureFactor.setEnabled(enabled)
3403
3404        self.cmdPlot.setEnabled(enabled)
3405
3406    def enableInteractiveElements(self):
3407        """
3408        Set buttion caption on fitting/calculate finish
3409        Enable the param table(s)
3410        """
3411        # Notify the user that fitting is available
3412        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3413        self.cmdFit.setText("Fit")
3414        self.fit_started = False
3415        self.setInteractiveElements(True)
3416
3417    def disableInteractiveElements(self):
3418        """
3419        Set buttion caption on fitting/calculate start
3420        Disable the param table(s)
3421        """
3422        # Notify the user that fitting is being run
3423        # Allow for stopping the job
3424        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3425        self.cmdFit.setText('Stop fit')
3426        self.setInteractiveElements(False)
3427
3428    def disableInteractiveElementsOnCalculate(self):
3429        """
3430        Set buttion caption on fitting/calculate start
3431        Disable the param table(s)
3432        """
3433        # Notify the user that fitting is being run
3434        # Allow for stopping the job
3435        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3436        self.cmdFit.setText('Running...')
3437        self.setInteractiveElements(False)
3438
3439    def readFitPage(self, fp):
3440        """
3441        Read in state from a fitpage object and update GUI
3442        """
3443        assert isinstance(fp, FitPage)
3444        # Main tab info
3445        self.logic.data.filename = fp.filename
3446        self.data_is_loaded = fp.data_is_loaded
3447        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3448        self.chkMagnetism.setCheckState(fp.is_magnetic)
3449        self.chk2DView.setCheckState(fp.is2D)
3450
3451        # Update the comboboxes
3452        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3453        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3454        if fp.current_factor:
3455            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3456
3457        self.chi2 = fp.chi2
3458
3459        # Options tab
3460        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3461        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3462        self.npts = fp.fit_options[fp.NPTS]
3463        self.log_points = fp.fit_options[fp.LOG_POINTS]
3464        self.weighting = fp.fit_options[fp.WEIGHTING]
3465
3466        # Models
3467        self._model_model = fp.model_model
3468        self._poly_model = fp.poly_model
3469        self._magnet_model = fp.magnetism_model
3470
3471        # Resolution tab
3472        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3473        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3474        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3475        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3476        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3477
3478        # TODO: add polidyspersity and magnetism
3479
3480    def saveToFitPage(self, fp):
3481        """
3482        Write current state to the given fitpage
3483        """
3484        assert isinstance(fp, FitPage)
3485
3486        # Main tab info
3487        fp.filename = self.logic.data.filename
3488        fp.data_is_loaded = self.data_is_loaded
3489        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3490        fp.is_magnetic = self.chkMagnetism.isChecked()
3491        fp.is2D = self.chk2DView.isChecked()
3492        fp.data = self.data
3493
3494        # Use current models - they contain all the required parameters
3495        fp.model_model = self._model_model
3496        fp.poly_model = self._poly_model
3497        fp.magnetism_model = self._magnet_model
3498
3499        if self.cbCategory.currentIndex() != 0:
3500            fp.current_category = str(self.cbCategory.currentText())
3501            fp.current_model = str(self.cbModel.currentText())
3502
3503        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3504            fp.current_factor = str(self.cbStructureFactor.currentText())
3505        else:
3506            fp.current_factor = ''
3507
3508        fp.chi2 = self.chi2
3509        fp.main_params_to_fit = self.main_params_to_fit
3510        fp.poly_params_to_fit = self.poly_params_to_fit
3511        fp.magnet_params_to_fit = self.magnet_params_to_fit
3512        fp.kernel_module = self.kernel_module
3513
3514        # Algorithm options
3515        # fp.algorithm = self.parent.fit_options.selected_id
3516
3517        # Options tab
3518        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3519        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3520        fp.fit_options[fp.NPTS] = self.npts
3521        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3522        fp.fit_options[fp.LOG_POINTS] = self.log_points
3523        fp.fit_options[fp.WEIGHTING] = self.weighting
3524
3525        # Resolution tab
3526        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3527        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3528        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3529        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3530        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3531
3532        # TODO: add polidyspersity and magnetism
3533
3534    def updateUndo(self):
3535        """
3536        Create a new state page and add it to the stack
3537        """
3538        if self.undo_supported:
3539            self.pushFitPage(self.currentState())
3540
3541    def currentState(self):
3542        """
3543        Return fit page with current state
3544        """
3545        new_page = FitPage()
3546        self.saveToFitPage(new_page)
3547
3548        return new_page
3549
3550    def pushFitPage(self, new_page):
3551        """
3552        Add a new fit page object with current state
3553        """
3554        self.page_stack.append(new_page)
3555
3556    def popFitPage(self):
3557        """
3558        Remove top fit page from stack
3559        """
3560        if self.page_stack:
3561            self.page_stack.pop()
3562
3563    def getReport(self):
3564        """
3565        Create and return HTML report with parameters and charts
3566        """
3567        index = None
3568        if self.all_data:
3569            index = self.all_data[self.data_index]
3570        else:
3571            index = self.theory_item
3572        params = FittingUtilities.getStandardParam(self._model_model)
3573        poly_params = []
3574        magnet_params = []
3575        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
3576            poly_params = FittingUtilities.getStandardParam(self._poly_model)
3577        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
3578            magnet_params = FittingUtilities.getStandardParam(self._magnet_model)
3579        report_logic = ReportPageLogic(self,
3580                                       kernel_module=self.kernel_module,
3581                                       data=self.data,
3582                                       index=index,
3583                                       params=params+poly_params+magnet_params)
3584
3585        return report_logic.reportList()
3586
3587    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3588        """
3589        This is a callback method called from the CANSAS reader.
3590        We need the instance of this reader only for writing out a file,
3591        so there's nothing here.
3592        Until Load Analysis is implemented, that is.
3593        """
3594        pass
3595
3596    def loadPageState(self, pagestate=None):
3597        """
3598        Load the PageState object and update the current widget
3599        """
3600        filepath = self.loadAnalysisFile()
3601        if filepath is None or filepath == "":
3602            return
3603
3604        with open(filepath, 'r') as statefile:
3605            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3606            lines = statefile.readlines()
3607
3608        # convert into list of lists
3609        pass
3610
3611    def loadAnalysisFile(self):
3612        """
3613        Called when the "Open Project" menu item chosen.
3614        """
3615        default_name = "FitPage"+str(self.tab_id)+".fitv"
3616        wildcard = "fitv files (*.fitv)"
3617        kwargs = {
3618            'caption'   : 'Open Analysis',
3619            'directory' : default_name,
3620            'filter'    : wildcard,
3621            'parent'    : self,
3622        }
3623        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3624        return filename
3625
3626    def onCopyToClipboard(self, format=None):
3627        """
3628        Copy current fitting parameters into the clipboard
3629        using requested formatting:
3630        plain, excel, latex
3631        """
3632        param_list = self.getFitParameters()
3633        if format=="":
3634            param_list = self.getFitPage()
3635            param_list += self.getFitModel()
3636            formatted_output = FittingUtilities.formatParameters(param_list)
3637        elif format == "Excel":
3638            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3639        elif format == "Latex":
3640            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3641        else:
3642            raise AttributeError("Bad parameter output format specifier.")
3643
3644        # Dump formatted_output to the clipboard
3645        cb = QtWidgets.QApplication.clipboard()
3646        cb.setText(formatted_output)
3647
3648    def getFitModel(self):
3649        """
3650        serializes combobox state
3651        """
3652        param_list = []
3653        model = str(self.cbModel.currentText())
3654        category = str(self.cbCategory.currentText())
3655        structure = str(self.cbStructureFactor.currentText())
3656        param_list.append(['fitpage_category', category])
3657        param_list.append(['fitpage_model', model])
3658        param_list.append(['fitpage_structure', structure])
3659
3660        return param_list
3661
3662    def getFitPage(self):
3663        """
3664        serializes full state of this fit page
3665        """
3666        # run a loop over all parameters and pull out
3667        # first - regular params
3668        param_list = self.getFitParameters()
3669
3670        param_list.append(['is_data', str(self.data_is_loaded)])
3671        data_ids = []
3672        filenames = []
3673        if self.is_batch_fitting:
3674            for item in self.all_data:
3675                # need item->data->data_id
3676                data = GuiUtils.dataFromItem(item)
3677                data_ids.append(data.id)
3678                filenames.append(data.filename)
3679        else:
3680            if self.data_is_loaded:
3681                data_ids = [str(self.logic.data.id)]
3682                filenames = [str(self.logic.data.filename)]
3683        param_list.append(['is_batch_fitting', str(self.is_batch_fitting)])
3684        param_list.append(['data_name', filenames])
3685        param_list.append(['data_id', data_ids])
3686        param_list.append(['tab_name', self.modelName()])
3687        # option tab
3688        param_list.append(['q_range_min', str(self.q_range_min)])
3689        param_list.append(['q_range_max', str(self.q_range_max)])
3690        param_list.append(['q_weighting', str(self.weighting)])
3691        param_list.append(['weighting', str(self.options_widget.weighting)])
3692
3693        # resolution
3694        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3695        index = self.smearing_widget.cbSmearing.currentIndex()
3696        param_list.append(['smearing', str(index)])
3697        param_list.append(['smearing_min', str(smearing_min)])
3698        param_list.append(['smearing_max', str(smearing_max)])
3699
3700        # checkboxes, if required
3701        has_polydisp = self.chkPolydispersity.isChecked()
3702        has_magnetism = self.chkMagnetism.isChecked()
3703        has_chain = self.chkChainFit.isChecked()
3704        has_2D = self.chk2DView.isChecked()
3705        param_list.append(['polydisperse_params', str(has_polydisp)])
3706        param_list.append(['magnetic_params', str(has_magnetism)])
3707        param_list.append(['chainfit_params', str(has_chain)])
3708        param_list.append(['2D_params', str(has_2D)])
3709
3710        return param_list
3711
3712    def getFitParameters(self):
3713        """
3714        serializes current parameters
3715        """
3716        param_list = []
3717        if self.kernel_module is None:
3718            return param_list
3719
3720        param_list.append(['model_name', str(self.cbModel.currentText())])
3721
3722        def gatherParams(row):
3723            """
3724            Create list of main parameters based on _model_model
3725            """
3726            param_name = str(self._model_model.item(row, 0).text())
3727
3728            # Assure this is a parameter - must contain a checkbox
3729            if not self._model_model.item(row, 0).isCheckable():
3730                # maybe it is a combobox item (multiplicity)
3731                try:
3732                    index = self._model_model.index(row, 1)
3733                    widget = self.lstParams.indexWidget(index)
3734                    if widget is None:
3735                        return
3736                    if isinstance(widget, QtWidgets.QComboBox):
3737                        # find the index of the combobox
3738                        current_index = widget.currentIndex()
3739                        param_list.append([param_name, 'None', str(current_index)])
3740                except Exception as ex:
3741                    pass
3742                return
3743
3744            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3745            # Value of the parameter. In some cases this is the text of the combobox choice.
3746            param_value = str(self._model_model.item(row, 1).text())
3747            param_error = None
3748            param_min = None
3749            param_max = None
3750            column_offset = 0
3751            if self.has_error_column:
3752                column_offset = 1
3753                param_error = str(self._model_model.item(row, 1+column_offset).text())
3754            try:
3755                param_min = str(self._model_model.item(row, 2+column_offset).text())
3756                param_max = str(self._model_model.item(row, 3+column_offset).text())
3757            except:
3758                pass
3759            # Do we have any constraints on this parameter?
3760            constraint = self.getConstraintForRow(row)
3761            cons = ()
3762            if constraint is not None:
3763                value = constraint.value
3764                func = constraint.func
3765                value_ex = constraint.value_ex
3766                param = constraint.param
3767                validate = constraint.validate
3768
3769                cons = (value, param, value_ex, validate, func)
3770
3771            param_list.append([param_name, param_checked, param_value,param_error, param_min, param_max, cons])
3772
3773        def gatherPolyParams(row):
3774            """
3775            Create list of polydisperse parameters based on _poly_model
3776            """
3777            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3778            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3779            param_value = str(self._poly_model.item(row, 1).text())
3780            param_error = None
3781            column_offset = 0
3782            if self.has_poly_error_column:
3783                column_offset = 1
3784                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3785            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3786            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3787            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3788            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3789            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3790            index = self._poly_model.index(row, 6+column_offset)
3791            widget = self.lstPoly.indexWidget(index)
3792            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3793                param_fun = widget.currentText()
3794            # width
3795            name = param_name+".width"
3796            param_list.append([name, param_checked, param_value, param_error,
3797                               param_min, param_max, param_npts, param_nsigs, param_fun])
3798
3799        def gatherMagnetParams(row):
3800            """
3801            Create list of magnetic parameters based on _magnet_model
3802            """
3803            param_name = str(self._magnet_model.item(row, 0).text())
3804            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3805            param_value = str(self._magnet_model.item(row, 1).text())
3806            param_error = None
3807            column_offset = 0
3808            if self.has_magnet_error_column:
3809                column_offset = 1
3810                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3811            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3812            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3813            param_list.append([param_name, param_checked, param_value,
3814                               param_error, param_min, param_max])
3815
3816        self.iterateOverModel(gatherParams)
3817        if self.chkPolydispersity.isChecked():
3818            self.iterateOverPolyModel(gatherPolyParams)
3819        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3820            self.iterateOverMagnetModel(gatherMagnetParams)
3821
3822        if self.kernel_module.is_multiplicity_model:
3823            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3824
3825        return param_list
3826
3827    def onParameterPaste(self):
3828        """
3829        Use the clipboard to update fit state
3830        """
3831        # Check if the clipboard contains right stuff
3832        cb = QtWidgets.QApplication.clipboard()
3833        cb_text = cb.text()
3834
3835        lines = cb_text.split(':')
3836        if lines[0] != 'sasview_parameter_values':
3837            return False
3838
3839        # put the text into dictionary
3840        line_dict = {}
3841        for line in lines[1:]:
3842            content = line.split(',')
3843            if len(content) > 1:
3844                line_dict[content[0]] = content[1:]
3845
3846        self.updatePageWithParameters(line_dict)
3847
3848    def createPageForParameters(self, line_dict):
3849        """
3850        Sets up page with requested model/str factor
3851        and fills it up with sent parameters
3852        """
3853        if 'fitpage_category' in line_dict:
3854            self.cbCategory.setCurrentIndex(self.cbCategory.findText(line_dict['fitpage_category'][0]))
3855        if 'fitpage_model' in line_dict:
3856            self.cbModel.setCurrentIndex(self.cbModel.findText(line_dict['fitpage_model'][0]))
3857        if 'fitpage_structure' in line_dict:
3858            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(line_dict['fitpage_structure'][0]))
3859
3860        # Now that the page is ready for parameters, fill it up
3861        self.updatePageWithParameters(line_dict)
3862
3863    def updatePageWithParameters(self, line_dict):
3864        """
3865        Update FitPage with parameters in line_dict
3866        """
3867        if 'model_name' not in line_dict.keys():
3868            return
3869        model = line_dict['model_name'][0]
3870        context = {}
3871
3872        if 'multiplicity' in line_dict.keys():
3873            multip = int(line_dict['multiplicity'][0], 0)
3874            # reset the model with multiplicity, so further updates are saved
3875            if self.kernel_module.is_multiplicity_model:
3876                self.kernel_module.multiplicity=multip
3877                self.updateMultiplicityCombo(multip)
3878
3879        if 'tab_name' in line_dict.keys():
3880            self.kernel_module.name = line_dict['tab_name'][0]
3881        if 'polydisperse_params' in line_dict.keys():
3882            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3883        if 'magnetic_params' in line_dict.keys():
3884            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3885        if 'chainfit_params' in line_dict.keys():
3886            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3887        if '2D_params' in line_dict.keys():
3888            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3889
3890        # Create the context dictionary for parameters
3891        context['model_name'] = model
3892        for key, value in line_dict.items():
3893            if len(value) > 2:
3894                context[key] = value
3895
3896        if str(self.cbModel.currentText()) != str(context['model_name']):
3897            msg = QtWidgets.QMessageBox()
3898            msg.setIcon(QtWidgets.QMessageBox.Information)
3899            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3900                         Not all parameters saved may paste correctly.")
3901            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3902            result = msg.exec_()
3903            if result == QtWidgets.QMessageBox.Ok:
3904                pass
3905            else:
3906                return
3907
3908        if 'smearing' in line_dict.keys():
3909            try:
3910                index = int(line_dict['smearing'][0])
3911                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3912            except ValueError:
3913                pass
3914        if 'smearing_min' in line_dict.keys():
3915            try:
3916                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3917            except ValueError:
3918                pass
3919        if 'smearing_max' in line_dict.keys():
3920            try:
3921                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3922            except ValueError:
3923                pass
3924
3925        if 'q_range_max' in line_dict.keys():
3926            try:
3927                self.q_range_min = float(line_dict['q_range_min'][0])
3928                self.q_range_max = float(line_dict['q_range_max'][0])
3929            except ValueError:
3930                pass
3931        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3932        try:
3933            button_id = int(line_dict['weighting'][0])
3934            for button in self.options_widget.weightingGroup.buttons():
3935                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3936                    button.setChecked(True)
3937                    break
3938        except ValueError:
3939            pass
3940
3941        self.updateFullModel(context)
3942        self.updateFullPolyModel(context)
3943        self.updateFullMagnetModel(context)
3944
3945    def updateMultiplicityCombo(self, multip):
3946        """
3947        Find and update the multiplicity combobox
3948        """
3949        index = self._model_model.index(self._n_shells_row, 1)
3950        widget = self.lstParams.indexWidget(index)
3951        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3952            widget.setCurrentIndex(widget.findText(str(multip)))
3953        self.current_shell_displayed = multip
3954
3955    def updateFullModel(self, param_dict):
3956        """
3957        Update the model with new parameters
3958        """
3959        assert isinstance(param_dict, dict)
3960        if not dict:
3961            return
3962
3963        def updateFittedValues(row):
3964            # Utility function for main model update
3965            # internal so can use closure for param_dict
3966            param_name = str(self._model_model.item(row, 0).text())
3967            if param_name not in list(param_dict.keys()):
3968                return
3969            # Special case of combo box in the cell (multiplicity)
3970            param_line = param_dict[param_name]
3971            if len(param_line) == 1:
3972                # modify the shells value
3973                try:
3974                    combo_index = int(param_line[0])
3975                except ValueError:
3976                    # quietly pass
3977                    return
3978                index = self._model_model.index(row, 1)
3979                widget = self.lstParams.indexWidget(index)
3980                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3981                    #widget.setCurrentIndex(combo_index)
3982                    return
3983            # checkbox state
3984            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3985            self._model_model.item(row, 0).setCheckState(param_checked)
3986
3987            # parameter value can be either just a value or text on the combobox
3988            param_text = param_dict[param_name][1]
3989            index = self._model_model.index(row, 1)
3990            widget = self.lstParams.indexWidget(index)
3991            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3992                # Find the right index based on text
3993                combo_index = int(param_text, 0)
3994                widget.setCurrentIndex(combo_index)
3995            else:
3996                # modify the param value
3997                param_repr = GuiUtils.formatNumber(param_text, high=True)
3998                self._model_model.item(row, 1).setText(param_repr)
3999
4000            # Potentially the error column
4001            ioffset = 0
4002            joffset = 0
4003            if len(param_dict[param_name])>5:
4004                # error values are not editable - no need to update
4005                ioffset = 1
4006            if self.has_error_column:
4007                joffset = 1
4008            # min/max
4009            try:
4010                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
4011                self._model_model.item(row, 2+joffset).setText(param_repr)
4012                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
4013                self._model_model.item(row, 3+joffset).setText(param_repr)
4014            except:
4015                pass
4016
4017            # constraints
4018            cons = param_dict[param_name][4+ioffset]
4019            if cons is not None and len(cons)==5:
4020                value = cons[0]
4021                param = cons[1]
4022                value_ex = cons[2]
4023                validate = cons[3]
4024                function = cons[4]
4025                constraint = Constraint()
4026                constraint.value = value
4027                constraint.func = function
4028                constraint.param = param
4029                constraint.value_ex = value_ex
4030                constraint.validate = validate
4031                self.addConstraintToRow(constraint=constraint, row=row)
4032
4033            self.setFocus()
4034
4035        self.iterateOverModel(updateFittedValues)
4036
4037    def updateFullPolyModel(self, param_dict):
4038        """
4039        Update the polydispersity model with new parameters, create the errors column
4040        """
4041        assert isinstance(param_dict, dict)
4042        if not dict:
4043            return
4044
4045        def updateFittedValues(row):
4046            # Utility function for main model update
4047            # internal so can use closure for param_dict
4048            if row >= self._poly_model.rowCount():
4049                return
4050            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
4051            if param_name not in list(param_dict.keys()):
4052                return
4053            # checkbox state
4054            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
4055            self._poly_model.item(row,0).setCheckState(param_checked)
4056
4057            # modify the param value
4058            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
4059            self._poly_model.item(row, 1).setText(param_repr)
4060
4061            # Potentially the error column
4062            ioffset = 0
4063            joffset = 0
4064            if len(param_dict[param_name])>7:
4065                ioffset = 1
4066            if self.has_poly_error_column:
4067                joffset = 1
4068            # min
4069            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
4070            self._poly_model.item(row, 2+joffset).setText(param_repr)
4071            # max
4072            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
4073            self._poly_model.item(row, 3+joffset).setText(param_repr)
4074            # Npts
4075            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
4076            self._poly_model.item(row, 4+joffset).setText(param_repr)
4077            # Nsigs
4078            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
4079            self._poly_model.item(row, 5+joffset).setText(param_repr)
4080
4081            self.setFocus()
4082
4083        self.iterateOverPolyModel(updateFittedValues)
4084
4085    def updateFullMagnetModel(self, param_dict):
4086        """
4087        Update the magnetism model with new parameters, create the errors column
4088        """
4089        assert isinstance(param_dict, dict)
4090        if not dict:
4091            return
4092
4093        def updateFittedValues(row):
4094            # Utility function for main model update
4095            # internal so can use closure for param_dict
4096            if row >= self._magnet_model.rowCount():
4097                return
4098            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
4099            if param_name not in list(param_dict.keys()):
4100                return
4101            # checkbox state
4102            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
4103            self._magnet_model.item(row,0).setCheckState(param_checked)
4104
4105            # modify the param value
4106            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
4107            self._magnet_model.item(row, 1).setText(param_repr)
4108
4109            # Potentially the error column
4110            ioffset = 0
4111            joffset = 0
4112            if len(param_dict[param_name])>4:
4113                ioffset = 1
4114            if self.has_magnet_error_column:
4115                joffset = 1
4116            # min
4117            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
4118            self._magnet_model.item(row, 2+joffset).setText(param_repr)
4119            # max
4120            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
4121            self._magnet_model.item(row, 3+joffset).setText(param_repr)
4122
4123        self.iterateOverMagnetModel(updateFittedValues)
4124
4125    def getCurrentFitState(self, state=None):
4126        """
4127        Store current state for fit_page
4128        """
4129        # save model option
4130        #if self.model is not None:
4131        #    self.disp_list = self.getDispParamList()
4132        #    state.disp_list = copy.deepcopy(self.disp_list)
4133        #    #state.model = self.model.clone()
4134
4135        # Comboboxes
4136        state.categorycombobox = self.cbCategory.currentText()
4137        state.formfactorcombobox = self.cbModel.currentText()
4138        if self.cbStructureFactor.isEnabled():
4139            state.structurecombobox = self.cbStructureFactor.currentText()
4140        state.tcChi = self.chi2
4141
4142        state.enable2D = self.is2D
4143
4144        #state.weights = copy.deepcopy(self.weights)
4145        # save data
4146        state.data = copy.deepcopy(self.data)
4147
4148        # save plotting range
4149        state.qmin = self.q_range_min
4150        state.qmax = self.q_range_max
4151        state.npts = self.npts
4152
4153        #    self.state.enable_disp = self.enable_disp.GetValue()
4154        #    self.state.disable_disp = self.disable_disp.GetValue()
4155
4156        #    self.state.enable_smearer = \
4157        #                        copy.deepcopy(self.enable_smearer.GetValue())
4158        #    self.state.disable_smearer = \
4159        #                        copy.deepcopy(self.disable_smearer.GetValue())
4160
4161        #self.state.pinhole_smearer = \
4162        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
4163        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
4164        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
4165        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
4166        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
4167        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
4168
4169        p = self.model_parameters
4170        # save checkbutton state and txtcrtl values
4171        state.parameters = FittingUtilities.getStandardParam(self._model_model)
4172        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
4173
4174        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
4175        #self._copy_parameters_state(self.parameters, self.state.parameters)
4176        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
4177        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
4178
Note: See TracBrowser for help on using the repository browser.