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

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

Added tooltip on COnstraints table.
Added "validate" parameter to Constraint, allowing for looser validation
of complex, multi-fitpage setups.

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