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

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

Initial commit of the dataset ordering functionality TRAC#933

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